The ultimate full week Heating/Thermostat control widget and ruleset (using Time-Profiles)

This widget/system uses my Time-Profiles, as presented in this post.

Design Goal:

I wanted to create a ruleset and widgets which allow Profile-based control of thermostats on a per-room basis that’s easily adjustable and responsive. Specifically, the system:

  • Allows users to adjust the currently active Profile and change it at will
  • Is responsive to other events, such as windows opening or a global vacation toggle
  • Enables different profiles on weekends automatically
  • Makes it simple to turn the automation in a specific room “off” or “on”
  • Allows extra features like a time-limited “boost” and “override” to comfort
  • Visualizes heating and schedule even for non-savvy users

The Widget

Demo Video

Main Widget

This widget is what the user sees in the MainUI when visiting the “Properties” tab of a location-card. Here the user can see the currently targeted temperature, and if they wish, enable the temporary “boost” or tweak the setting using the override slider. Additionally, the user can see on the weekly calendar what profiles will be active on which days of the week, and the temperature range that they travel is shown by the differently colored red highlighting on the graph. Should the user wish to change the profile, the settings button opens up the settings widget, as shown below.

Settings Widget

The Settings widget enables the user to fine-tune the control over the room’s thermostat control. Here, different Modes can be selected which determine the Profile choice of each weekday. “Static” mode will simply always use the primary Profile each day, while setting the zone to “M-F / S-S” allows a secondary Profile to be set that is used on weekends. Finally, the “Default” mode is currently unused as to leave space for future development and features should the need arise.

Source Code

Note that this system is deeply intertwined with the “Profiles” system, it is a prerequisite.

YAML Widgets

Main Widget

uid: temperature_control_v2
tags: []
props:
  parameters:
    - context: item
      description: Heating zone to pattern
      label: Item
      name: item
      required: false
      type: TEXT
  parameterGroups: []
timestamp: Dec 22, 2022, 4:27:12 PM
component: f7-list-item
config:
  class: media-item
slots:
  content:
    - component: f7-block
      config:
        style:
          background: "#232323"
          border-bottom: "1px solid #3e3e3f"
          border-top: "1px solid #3e3e3f"
          margin-bottom: 10px
          margin-left: -16px
          margin-right: -16px
          margin-top: -8px
      slots:
        default:
          - component: f7-row
            config:
              style:
                height: 2.5em
                position: relative
            slots:
              default:
                - component: f7-col
                  config:
                    style:
                      height: 2em
                      margin-top: 0.25em
                      padding: 6px
                      width: 80%
                  slots:
                    default:
                      - component: f7-row
                        slots:
                          default:
                            - component: f7-col
                              config:
                                style:
                                  font-size: 11pt
                                  text-align: Left
                              slots:
                                default:
                                  - component: Label
                                    config:
                                      style:
                                        float: left
                                        font-weight: bold
                                        padding-right: 10px
                                      text: =props.item.split('_')[0]  + ':'
                                  - component: Label
                                    config:
                                      text: Heat Control
                - component: f7-col
                  config:
                    style:
                      padding: 6px
                      text-align: right
                      vertical-align: center
                      width: 20%
                  slots:
                    default:
                      - component: oh-toggle
                        config:
                          color: red
                          item: =props.item
                          style:
                            height: 100%
    - component: f7-block
      config:
        style:
          margin-bottom: 10px
      slots:
        default:
          - component: f7-row
            config:
              style:
                margin-left: -16px
                margin-right: -16px
            slots:
              default:
                - component: f7-col
                  config:
                    style:
                      background: "#232323"
                      border-radius: 0.7em
                      height: 3.3em
                  slots:
                    default:
                      - component: Label
                        config:
                          style:
                            font-size: 8pt
                            text-align: center
                          text: Target
                      - component: Label
                        config:
                          style:
                            font-size: 16pt
                            text-align: center
                          text: =items[props.item.split('_')[0] + '_Heating_Target'].state + '°C'
                - component: f7-col
                  config:
                    style:
                      background: "=( items[props.item.split('_')[0] + '_Heating_BoostMode'].state == 'ON' ? '#2e853d' : '#232323' )"
                      border-radius: 0.7em
                      height: 3.3em
                  slots:
                    default:
                      - component: f7-row
                        config:
                          style:
                            height: 1.2em
                        slots:
                          default:
                            - component: f7-col
                              slots:
                                default:
                                  - component: Label
                                    config:
                                      style:
                                        font-size: 8pt
                                        text-align: center
                                      text: Boost
                      - component: f7-row
                        slots:
                          default:
                            - component: f7-col
                              config:
                                style:
                                  text-align: center
                              slots:
                                default:
                                  - component: oh-toggle
                                    config:
                                      color: green
                                      item: =props.item.split('_')[0] + '_Heating_BoostMode'
                - component: f7-col
                  config:
                    style:
                      background: "#232323"
                      border-radius: 0.7em
                      height: 3.3em
                      text-align: center
                      vertical-align: center
                      width: 20%
                  slots:
                    default:
                      - component: oh-button
                        config:
                          action: popup
                          actionModal: widget:temperature_control_v1_popup
                          actionModalConfig:
                            root_item: =props.item
                          color: white
                          iconF7: gear_alt_fill
                          large: true
                          style:
                            border-radius: 0.7em
                            height: 3.3em
    - component: f7-block
      config:
        style:
          margin-left: -1em
          margin-right: -1em
          text-align: center
      slots:
        default:
          - component: f7-row
            config:
              style:
                position: relative
            slots:
              default:
                - component: Label
                  config:
                    style:
                      background: "#232323"
                      font-size: 6pt
                      left: 0px
                      padding: 3px
                      position: absolute
                      top: 36%
                    text: 20°C
                - component: Label
                  config:
                    style:
                      background: "#232323"
                      font-size: 6pt
                      left: 0px
                      padding: 3px
                      position: absolute
                      top: 59%
                    text: 15°C
                - component: oh-repeater
                  config:
                    for: count
                    fragment: true
                    rangeStart: 1
                    rangeStep: 1
                    rangeStop: 7
                    sourceType: range
                  slots:
                    default:
                      - component: f7-col
                        config:
                          style:
                            --f7-grid-gap: 0.1em
                            background: "#232323"
                            border-radius: "=(loop.count == '1' ? '.7em 0em 0em .7em' : (loop.count == '7' ? '0em .7em .7em 0em' : '0em 0em 0em 0em'))"
                            margin-bottom: 1em
                            outline: "=( (dayjs().day() == 0 ? 7 : dayjs().day()) == loop.count ? '1px dashed white' : '')"
                        slots:
                          default:
                            - component: f7-row
                              slots:
                                default:
                                  - component: f7-col
                                    slots:
                                      default:
                                        - component: Label
                                          config:
                                            style:
                                              background: "#3e3e3f"
                                              border-radius: "=(loop.count == '1' ? '.7em 0em 0em 0em' : (loop.count == '7' ? '0em .7em 0em 0em' : '0em 0em 0em 0em'))"
                                              font-weight: bold
                                            text: "=dayjs().day(loop.count).format('ddd').toUpperCase() "
                            - component: f7-row
                              config:
                                style:
                                  background: linear-gradient(0deg, rgba(35,35,35,1) 0%, rgba(35,35,35,1) 32%, rgba(255,255,255,1) 33%, rgba(35,35,35,1) 34%, rgba(35,35,35,1) 65%, rgba(255,255,255,1) 66%, rgba(35,35,35,1) 67%, rgba(35,35,35,1) 100%)
                                  border-radius: "=(loop.count == '1' ? '0em 0em 0em .7em' : (loop.count == '7' ? '0em 0em .7em 0em' : '0em 0em 0em 0em'))"
                              slots:
                                default:
                                  - component: f7-col
                                    config:
                                      style:
                                        border-radius: "=(loop.count == '1' ? '0em 0em 0em .7em' : (loop.count == '7' ? '0em 0em .7em 0em' : '0em 0em 0em 0em'))"
                                        height: 6em
                                        position: relative
                                    slots:
                                      default:
                                        - component: f7-block
                                          config:
                                            style:
                                              background: "=( (items[props.item.split('_')[0] + '_Heating_Active'].state == 'ON' ? ( loop.count == (dayjs().day() == 0 ? 7 : dayjs().day()) ? items[props.item.split('_')[0] + '_Heating_AutomationProfileActive'].state : ( items[props.item.split('_')[0] + '_Heating_AutomationMode'].state == 'Auto' ? (loop.count < 6 ? items[props.item.split('_')[0] + '_Heating_AutomationProfilePrimary'].state : items[props.item.split('_')[0] + '_Heating_AutomationProfileAlternate'].state) : items[props.item.split('_')[0] + '_Heating_AutomationProfilePrimary'].state ) ) : 'Frostguard') == 'Frostguard' ? 'rgba(48, 48, 227, 0.9)' : 'rgba(227, 48, 48, 0.9)')"
                                              border-radius: "=(loop.count == '1' ? '0em 0em 0em .7em' : (loop.count == '7' ? '0em 0em .7em 0em' : '0em 0em 0em 0em'))"
                                              bottom: 0px
                                              height: "=( items[props.item.split('_')[0] + '_Heating_Active'].state == 'ON' ? ( loop.count == (dayjs().day() == 0 ? 7 : dayjs().day()) ? ( items[items[props.item.split('_')[0] + '_Heating_AutomationProfileActiveId'].state + '_MinValue'].state ) : ( items[props.item.split('_')[0] + '_Heating_AutomationMode'].state == 'Auto' ? ( loop.count < 6 ? ( items[items[props.item.split('_')[0] + '_Heating_AutomationProfilePrimaryId'].state + '_MinValue'].state ) : ( items[items[props.item.split('_')[0] + '_Heating_AutomationProfileAlternateId'].state + '_MinValue'].state ) ) : ( items[items[props.item.split('_')[0] + '_Heating_AutomationProfilePrimaryId'].state + '_MinValue'].state ) ) ) : ( items[items[props.item.split('_')[0] + '_Heating_AutomationProfileActiveId'].state + '_MinValue'].state ) ) * 0.4 - 4 + 'em'"
                                              position: absolute
                                              width: 100%
                                        - component: f7-block
                                          config:
                                            style:
                                              background: rgba(255, 0, 0, 0.4)
                                              bottom: "=( items[props.item.split('_')[0] + '_Heating_Active'].state == 'ON' ? ( loop.count == (dayjs().day() == 0 ? 7 : dayjs().day()) ? ( items[items[props.item.split('_')[0] + '_Heating_AutomationProfileActiveId'].state + '_MinValue'].state ) : ( items[props.item.split('_')[0] + '_Heating_AutomationMode'].state == 'Auto' ? ( loop.count < 6 ? ( items[items[props.item.split('_')[0] + '_Heating_AutomationProfilePrimaryId'].state + '_MinValue'].state ) : ( items[items[props.item.split('_')[0] + '_Heating_AutomationProfileAlternateId'].state + '_MinValue'].state ) ) : ( items[items[props.item.split('_')[0] + '_Heating_AutomationProfilePrimaryId'].state + '_MinValue'].state ) ) ) : ( items[items[props.item.split('_')[0] + '_Heating_AutomationProfileActiveId'].state + '_MinValue'].state ) ) * 0.4 - 4 + 'em'"
                                              height: "=( ( ( items[props.item.split('_')[0] + '_Heating_Active'].state == 'ON' ? ( loop.count == (dayjs().day() == 0 ? 7 : dayjs().day()) ? ( items[items[props.item.split('_')[0] + '_Heating_AutomationProfileActiveId'].state + '_MaxValue'].state ) : ( items[props.item.split('_')[0] + '_Heating_AutomationMode'].state == 'Auto' ? ( loop.count < 6 ? ( items[items[props.item.split('_')[0] + '_Heating_AutomationProfilePrimaryId'].state + '_MaxValue'].state ) : ( items[items[props.item.split('_')[0] + '_Heating_AutomationProfileAlternateId'].state + '_MaxValue'].state ) ) : ( items[items[props.item.split('_')[0] + '_Heating_AutomationProfilePrimaryId'].state + '_MaxValue'].state ) ) ) : ( items[items[props.item.split('_')[0] + '_Heating_AutomationProfileActiveId'].state + '_MaxValue'].state ) ) * 0.4 - 4 ) - ( ( items[props.item.split('_')[0] + '_Heating_Active'].state == 'ON' ? ( loop.count == (dayjs().day() == 0 ? 7 : dayjs().day()) ? ( items[items[props.item.split('_')[0] + '_Heating_AutomationProfileActiveId'].state + '_MinValue'].state ) : ( items[props.item.split('_')[0] + '_Heating_AutomationMode'].state == 'Auto' ? ( loop.count < 6 ? ( items[items[props.item.split('_')[0] + '_Heating_AutomationProfilePrimaryId'].state + '_MinValue'].state ) : ( items[items[props.item.split('_')[0] + '_Heating_AutomationProfileAlternateId'].state + '_MinValue'].state ) ) : ( items[items[props.item.split('_')[0] + '_Heating_AutomationProfilePrimaryId'].state + '_MinValue'].state ) ) ) : ( items[items[props.item.split('_')[0] + '_Heating_AutomationProfileActiveId'].state + '_MinValue'].state ) ) * 0.4 - 4) ) + 'em'"
                                              position: absolute
                                              width: 100%
                                        - component: Label
                                          config:
                                            style:
                                              bottom: 0px
                                              font-size: 8pt
                                              left: 0px
                                              position: absolute
                                              right: 0px
                                              z-index: 1
                                            text: "=( (items[props.item.split('_')[0] + '_Heating_Active'].state == 'ON' ? ( loop.count == (dayjs().day() == 0 ? 7 : dayjs().day()) ? items[props.item.split('_')[0] + '_Heating_AutomationProfileActive'].state : ( items[props.item.split('_')[0] + '_Heating_AutomationMode'].state == 'Auto' ? (loop.count < 6 ? items[props.item.split('_')[0] + '_Heating_AutomationProfilePrimary'].state : items[props.item.split('_')[0] + '_Heating_AutomationProfileAlternate'].state) : items[props.item.split('_')[0] + '_Heating_AutomationProfilePrimary'].state ) ) : '❄️') == 'Frostguard' ? '️❄️' : (items[props.item.split('_')[0] + '_Heating_Active'].state == 'ON' ? ( loop.count == (dayjs().day() == 0 ? 7 : dayjs().day()) ? items[props.item.split('_')[0] + '_Heating_AutomationProfileActive'].state : ( items[props.item.split('_')[0] + '_Heating_AutomationMode'].state == 'Auto' ? (loop.count < 6 ? items[props.item.split('_')[0] + '_Heating_AutomationProfilePrimary'].state : items[props.item.split('_')[0] + '_Heating_AutomationProfileAlternate'].state) : items[props.item.split('_')[0] + '_Heating_AutomationProfilePrimary'].state ) ) : '❄️'))"
    - component: f7-block
      config:
        style:
          background: "#232323"
          border-radius: 0.7em
          height: 3.7em
          text-align: center
      slots:
        default:
          - component: Label
            config:
              style:
                margin-bottom: -8px
                width: 100%
              text: ◄◄ Override ►►
          - component: f7-col
            config:
              style:
                margin: 6px
            slots:
              default:
                - component: oh-slider
                  config:
                    item: =props.item.split('_')[0] + '_Heating_Offset'
                    label: true
                    max: 3
                    min: -3
                    releaseOnly: true
                    scale: true
                    scaleSteps: 6
                    step: 0.5
                    style:
                      --f7-range-bar-active-bg-color: "#919191"
                      --f7-range-bar-bg-color: "#919191"
                    unit: °C

Popup Widget

uid: temperature_control_v1_popup
tags: []
props:
  parameters:
    - context: item
      description: The master item for the heating zone
      label: Master_Heating_Switch item
      name: root_item
      required: false
      type: TEXT
  parameterGroups: []
timestamp: Dec 22, 2022, 4:32:40 PM
component: f7-card
config:
  style:
    text-align: center
  text: "='Thermostat Control: ' + props.root_item.split('_')[0]"
slots:
  content:
    - component: f7-block
      config:
        style:
          margin-left: -2em
          margin-right: -2em
      slots:
        default:
          - component: f7-row
            config:
              style:
                margin-bottom: 20px
                margin-top: 10px
                text-align: center
                width: 100%
            slots:
              default:
                - component: f7-col
                  config:
                    style:
                      margin-left: 20px
                      text-align: right
                  slots:
                    default:
                      - component: Label
                        config:
                          text: "Zone Active:"
                - component: f7-col
                  config:
                    style:
                      text-align: left
                  slots:
                    default:
                      - component: oh-toggle
                        config:
                          item: =props.root_item
                - component: f7-col
                  config:
                    style:
                      text-align: right
                  slots:
                    default:
                      - component: Label
                        config:
                          text: "Boost:"
                - component: f7-col
                  config:
                    style:
                      text-align: left
                  slots:
                    default:
                      - component: oh-toggle
                        config:
                          item: =props.root_item.split('_')[0] + '_Heating_BoostMode'
          - component: f7-row
            config:
              style:
                margin-top: 10px
            slots:
              default:
                - component: f7-col
                  config:
                    style:
                      border-right: 2px solid white
                      text-align: center
                      width: 50%
                  slots:
                    default:
                      - component: f7-row
                        slots:
                          default:
                            - component: Label
                              config:
                                style:
                                  width: 100%
                                text: Measured
                      - component: f7-row
                        config:
                          style:
                            text-align: center
                        slots:
                          default:
                            - component: Label
                              config:
                                style:
                                  font-size: 18pt
                                  font-weight: bold
                                  width: 100%
                                text: =items[props.root_item.split('_')[0] + '_Temperature'].state + ' °C'
                - component: f7-col
                  config:
                    style:
                      width: 50%
                  slots:
                    default:
                      - component: f7-row
                        slots:
                          default:
                            - component: Label
                              config:
                                style:
                                  width: 100%
                                text: Target
                      - component: f7-row
                        slots:
                          default:
                            - component: Label
                              config:
                                style:
                                  font-size: 18pt
                                  font-weight: bold
                                  width: 100%
                                text: =items[props.root_item.split('_')[0] + '_Heating_Target'].state + ' °C'
          - component: f7-row
            config:
              style:
                margin-top: 20px
                text-align: center
                width: 100%
            slots:
              default:
                - component: Label
                  config:
                    style:
                      margin-bottom: -15px
                      width: 100%
                    text: ◄◄ Override ►►
                - component: f7-col
                  config:
                    style:
                      margin: 10px
                      padding: 10px
                  slots:
                    default:
                      - component: oh-slider
                        config:
                          item: =props.root_item.split('_')[0] + '_Heating_Offset'
                          label: true
                          max: 3
                          min: -3
                          releaseOnly: true
                          scale: true
                          scaleSteps: 6
                          step: 0.5
                          style:
                            --f7-range-bar-active-bg-color: "#919191"
                            --f7-range-bar-bg-color: "#919191"
                          unit: °C
          - component: f7-row
            config:
              style:
                margin-top: 10px
                text-align: center
                width: 100%
            slots:
              default:
                - component: f7-segmented
                  config:
                    round: true
                    style:
                      padding: 10px
                      width: 100%
                  slots:
                    default:
                      - component: oh-button
                        config:
                          action: command
                          actionCommand: Static
                          actionItem: =props.root_item.split('_')[0] + '_Heating_AutomationMode'
                          active: "=(items[props.root_item.split('_')[0] + '_Heating_AutomationMode'].state == 'Static') ? true : false"
                          outline: true
                          round: true
                          text: Static
                      - component: oh-button
                        config:
                          action: command
                          actionCommand: Auto
                          actionItem: =props.root_item.split('_')[0] + '_Heating_AutomationMode'
                          active: "=(items[props.root_item.split('_')[0] + '_Heating_AutomationMode'].state == 'Auto') ? true : false"
                          outline: true
                          round: true
                          text: M-F / S-S
                      - component: oh-button
                        config:
                          action: command
                          actionCommand: Smart
                          actionItem: =props.root_item.split('_')[0] + '_Heating_AutomationMode'
                          active: "=(items[props.root_item.split('_')[0] + '_Heating_AutomationMode'].state == 'Smart') ? true : false"
                          outline: true
                          round: true
                          text: Default
          - component: f7-row
            config:
              style:
                margin-top: 20px
                text-align: center
                width: 100%
            slots:
              default:
                - component: f7-col
                  slots:
                    default:
                      - component: Label
                        config:
                          style:
                            text-align: right
                          text: "Active Profile:"
                - component: f7-col
                  slots:
                    default:
                      - component: f7-row
                        slots:
                          default:
                            - component: Label
                              config:
                                style:
                                  background-color: "=( items[props.root_item.split('_')[0] + '_Heating_AutomationProfileActive'].state == 'Away' ? '#919191' : ( items[props.root_item.split('_')[0] + '_Heating_AutomationProfileActive'].state == 'Frostguard' ? '#00ACB5' : ( items[props.root_item.split('_')[0] + '_Heating_AutomationProfileActive'].state == 'Present' ? '#008c09' : '#902022')))"
                                  border-radius: 10px
                                  font-weight: bold
                                  padding: 0px 10px 0px 10px
                                text: =items[props.root_item.split('_')[0] + '_Heating_AutomationProfileActive'].state
          - component: f7-row
            config:
              style:
                margin-bottom: 10px
                margin-top: 10px
            slots:
              default:
                - component: f7-col
                  config:
                    style:
                      text-align: center
                      width: 50%
                  slots:
                    default:
                      - component: f7-row
                        slots:
                          default:
                            - component: Label
                              config:
                                style:
                                  width: 100%
                                text: Workday
                      - component: f7-row
                        config:
                          style:
                            justify-content: center
                            margin-top: 10px
                            padding: 0px 20px 0px 20px
                            text-align: center
                        slots:
                          default:
                            - component: oh-button
                              config:
                                action: popup
                                actionModal: widget:temperature_control_v1_profilepopup
                                actionModalConfig:
                                  item: =props.root_item.split('_')[0] + '_Heating_AutomationProfilePrimary'
                                fill: true
                                round: true
                                style:
                                  background-color: "=( items[props.root_item.split('_')[0] + '_Heating_AutomationProfilePrimary'].state == 'Away' ? '#919191' : ( items[props.root_item.split('_')[0] + '_Heating_AutomationProfilePrimary'].state == 'Frostguard' ? '#00ACB5' : '#902022'))"
                                  font-weight: bold
                                text: =items[props.root_item.split('_')[0] + '_Heating_AutomationProfilePrimary'].state
                - component: f7-col
                  config:
                    style:
                      width: 50%
                  slots:
                    default:
                      - component: f7-row
                        slots:
                          default:
                            - component: Label
                              config:
                                style:
                                  width: 100%
                                text: Weekend
                      - component: f7-row
                        config:
                          style:
                            justify-content: center
                            margin-top: 10px
                            padding: 0px 20px 0px 20px
                            text-align: center
                        slots:
                          default:
                            - component: oh-button
                              config:
                                action: popup
                                actionModal: widget:temperature_control_v1_profilepopup
                                actionModalConfig:
                                  item: =props.root_item.split('_')[0] + '_Heating_AutomationProfileAlternate'
                                fill: true
                                round: true
                                style:
                                  background-color: "=( items[props.root_item.split('_')[0] + '_Heating_AutomationProfileAlternate'].state == 'Away' ? '#919191' : ( items[props.root_item.split('_')[0] + '_Heating_AutomationProfileAlternate'].state == 'Frostguard' ? '#00ACB5' : '#902022'))"
                                  font-weight: bold
                                text: =items[props.root_item.split('_')[0] + '_Heating_AutomationProfileAlternate'].state
          - component: f7-row
            config:
              style:
                margin-bottom: 10px
                margin-top: 10px
            slots:
              default:
                - component: f7-col
                  slots:
                    default:
                      - component: widget:profile_graph
                        config:
                          profilecore: =items[props.root_item.split('_')[0] + '_Heating_AutomationProfileActiveId'].state

Profile Selector

uid: temperature_control_v1_profilepopup
tags: []
props:
  parameters:
    - context: item
      description: Profile Item to be adjusted
      label: Item
      name: item
      required: false
      type: TEXT
  parameterGroups: []
timestamp: Dec 22, 2022, 4:26:57 PM
component: f7-card
config:
  title: "Select a profile:"
slots:
  default:
    - component: f7-list
      slots:
        default:
          - component: oh-repeater
            config:
              filter: items[loop.item.name + '_Type'].state == 'Temperature'
              for: item
              fragment: true
              groupItem: ProfileList
              sourceType: itemsInGroup
            slots:
              default:
                - component: f7-list-item
                  slots:
                    default:
                      - component: oh-button
                        config:
                          action: command
                          actionCommand: =items[loop.item.name + '_Name'].state
                          actionItem: =props.item
                          fill: "=(items[props.item].state == items[loop.item.name + '_Name'].state ? true : false)"
                          style:
                            width: 100%
                          text: =items[loop.item.name + '_Name'].state

Virtual Items

.items setup for zones

//Heating Control Items (Virtual)
//Global Groups
Switch Global_Vacation_Override "Set all temperature zones to frostguard"
Group Sensor "Sensors"
Group Temperature "Temperature" (Sensor)
Group Humidity "Humidity" (Sensor) 

//Heat Specific Groups
Group Heating_Input
Switch Heat_Debug
Group Master_Heating_Switch 

//Heating items for Guest Bedroom

Group:Number:AVG GuestBedroom_Temperature "Average Temperature" <temperature> (GuestBedroom,Sensor,Temperature)  
Group:Number:AVG GuestBedroom_Humidity "Average Humidity" <humidity> (GuestBedroom,Sensor,Humidity)  

Switch GuestBedroom_Heating_Active "Heating Active" (GuestBedroom,Heating_Input,Master_Heating_Switch) ["Temperature"] {listWidget="widget:temperature_control_v2"[item="GuestBedroom_Heating_Active"]}
Number GuestBedroom_Heating_Offset "Heating Offset" (GuestBedroom,Heating_Input)  
Number GuestBedroom_Heating_Target "Heating Target" (GuestBedroom,Heating_Input)  
String GuestBedroom_Heating_AutomationMode "Heating Automation Mode" (GuestBedroom,Heating_Input)  
String GuestBedroom_Heating_AutomationProfilePrimary "Heating Automation Primary Profile" (GuestBedroom,Heating_Input)  
String GuestBedroom_Heating_AutomationProfileAlternate "Heating Automation Alternate Profile" (GuestBedroom,Heating_Input)  
String GuestBedroom_Heating_AutomationProfileActive "Heating Automation Active Profile" (GuestBedroom,Heating_Input)  
String GuestBedroom_Heating_AutomationProfileActiveId "Heating Automation Active Profile ID" (GuestBedroom,Heating_Input)  
String GuestBedroom_Heating_AutomationProfilePrimaryId "Heating Automation Primary Profile ID" (GuestBedroom,Heating_Input)  
String GuestBedroom_Heating_AutomationProfileAlternateId "Heating Automation Alternate Profile ID" (GuestBedroom,Heating_Input)  
Switch GuestBedroom_Heating_BoostMode "Heating Boost Mode" (GuestBedroom,Heating_Input)  
Switch GuestBedroom_Heating_WindowSuspension "Heating Window Suspension" (GuestBedroom,Heating_Input)  

Rules Code

heatAutomate.rules

import org.openhab.core.model.script.ScriptServiceUtil
//Heat automation rules
//ScriptServiceUtil.getItemRegistry.getItem( ) as GenericItem
//Thermo update rules:
val rfDelay = 100

//This rule runs once every ten minutes and updates all the thermostats to their appropriate targets
rule "Heat Automation - Interval Update GLOBAL"
when
    Item Heat_Debug received command ON or
    Time cron "0 0/10 * 1/1 * ? *"
then
    logInfo("Heat Automation","Running global Heat-Automation update")
    if(SystemReady.state != ON){
        logError("System Startup Catch", "Rule execution blocked -- Persistent variables not yet loaded!")
    }else{
        //Wait a second or so for other things to happen
        Thread::sleep(2000)

        //For debugging purposes, set this to true
        val debugmode = false

        
        Master_Heating_Switch.members.forEach[ root |
            var root_room =  "" + root.name.split('_').get(0)

            //First, check for Null items and fix them if necessary
            if(root.state == NULL){
                logWarn("Null-Catcher", "Caught a NULL heating zone in " + root_room + "!")
                ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_Active").postUpdate(OFF)
                ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_Offset").postUpdate(0.0)
                ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_AutomationMode").postUpdate("Static")
                ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_AutomationProfilePrimary").postUpdate("Away")
                ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_AutomationProfileAlternate").postUpdate("Away")
                ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_WindowSuspension").postUpdate(OFF)
                //Note that the target and the active profile are not set, even in null cases. This is because they will later be calculated
                logWarn("Null-Catcher", "Catch Completed")
            }

            // ====== STEP 1 ======
            // Determine the active profile and error-check it
            if(debugmode){logInfo("Heat Automation", "Beginning Step 1 for " + root_room)}

            val AutomationModeItem = ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_AutomationMode") as GenericItem
            var AutomationMode = AutomationModeItem.state.toString
            val ActiveProfileItem = ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_AutomationProfileActive") as GenericItem

            //Quickly error-check the mode
            if(AutomationModeItem.state == NULL || !(AutomationMode == "Static" || AutomationMode == "Auto" || AutomationMode == "Smart")){
                logWarn("Heating Automation", "Mode error in " + AutomationModeItem.name + "! Setting it to Static from " + AutomationModeItem.state)
                AutomationModeItem.postUpdate("Static")
                AutomationMode = "Static"
            }

            var ActiveProfile = ""
            //Check to see if the window suspension is on. If it is, set the profile to Frostguard
            val WindowSuspensionItem = ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_WindowSuspension")
            var BoostMode = ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_BoostMode").state
            //Catch NULL
            if(BoostMode == NULL){
                BoostMode = OFF 
                ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_BoostMode").postUpdate(OFF)
            }

            val HeatActiveSwitch = ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_Active") as GenericItem
            if(HeatActiveSwitch.state == NULL){
                HeatActiveSwitch.postUpdate(OFF)
                
                ActiveProfile = "Frostguard"
                ActiveProfileItem.postUpdate(ActiveProfile)
            }

            if(HeatActiveSwitch.state == ON){
                if(WindowSuspensionItem.state == NULL){
                    WindowSuspensionItem.postUpdate(OFF)
                }else if(WindowSuspensionItem.state == ON){
                    //Window Suspension mode is on, override to Frostguard
                    ActiveProfileItem.postUpdate("Frostguard")
                    ActiveProfile = "Frostguard"
                }else{
                    //Check to see if boost mode is active 
                    if(BoostMode == ON){
                        ActiveProfileItem.postUpdate("Present")
                        ActiveProfile = "Present"
                    }else{
                        //window Suspension is not active
                        if(AutomationMode == "Static"){
                            //Automation mode is Static, pass through the primary profile
                            ActiveProfile = ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_AutomationProfilePrimary").state.toString
                            ActiveProfileItem.postUpdate(ActiveProfile)
                        }else if(AutomationMode == "Auto"){
                            //Determine if today is a workday or weekend day
                            if(now.getDayOfWeek.getValue >= 6){
                                //It's the weekend, move the Alternate profile into the Active one
                                ActiveProfile = ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_AutomationProfileAlternate").state.toString
                                ActiveProfileItem.postUpdate(ActiveProfile)
                            }else{
                                //It's a workday, move the Primary profile into the Active one
                                ActiveProfile = ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_AutomationProfilePrimary").state.toString
                                ActiveProfileItem.postUpdate(ActiveProfile)
                            }
                        }else if(AutomationMode == "Smart"){
                            //The mode is set to smart
                            // TODO: Implement features, right now just immitates Static mode
                            ActiveProfile = ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_AutomationProfilePrimary").state.toString
                            ActiveProfileItem.postUpdate(ActiveProfile)
                        }
                    }
                }
            }else{
                ActiveProfile = "Frostguard"
                ActiveProfileItem.postUpdate(ActiveProfile)
            }

            //Check to see if the target profile is a real one
            var foundProfileInList = false 
            var ProfileItemName = ""
            var CalculationFetched = new DecimalType(0.0)

            //if(ActiveProfile == "Away" || ActiveProfile == "Frostguard" || ActiveProfile == "Present"){
            //    //We're good, no need to do anything. 
            //    ProfileItemName = "None"
            //    foundProfileInList = true
            //}else{
                //Search the list of profiles for one with a matching name and type
                ProfileList.members.forEach[ GroupItem d |
                    //logInfo("Debug","Checking... " + d.name)

                    var ProfileName = ScriptServiceUtil.getItemRegistry.getItem("" + d.name + "_Name" ).state.toString
                    var ProfileType = ScriptServiceUtil.getItemRegistry.getItem("" + d.name + "_Type" ).state.toString
                    if(ProfileType == "Temperature" && ProfileName.equals(ActiveProfile)){
                        foundProfileInList = true
                        ProfileItemName = d.name
                        CalculationFetched = ScriptServiceUtil.getItemRegistry.getItem("" + d.name + "_Calculated" ).state as DecimalType
                    }
                ]
            //}

            //Do Global vacation check
            if(Global_Vacation_Override.state == NULL){
                Global_Vacation_Override.postUpdate(OFF)
            }
            if(Global_Vacation_Override.state == ON){
                //On vacation, turn everything to frostguard
                ActiveProfile = "Frostguard"
                ActiveProfileItem.postUpdate(ActiveProfile)
            }

            //Do Local Heating Active Check
            if(HeatActiveSwitch.state == OFF){
                ActiveProfile = "Frostguard"
                ActiveProfileItem.postUpdate(ActiveProfile)
            }

            //Error found, handle it
            if(!foundProfileInList){
                logWarn("Heat Automation", "Profile \"" + ActiveProfile + "\" not found in list of profiles. Using \"Frostguard\" Profile as stand in")
                ActiveProfile = "Frostguard"
                ProfileItemName = "None"
                ActiveProfileItem.postUpdate(ActiveProfile)
            }

            //Compute the ID items
            /*
            String TilmanRoom_Heating_AutomationProfileActiveId "Heating Automation Active Profile ID" (TilmanRoom,Heating_Input)  
            String TilmanRoom_Heating_AutomationProfilePrimaryId "Heating Automation Primary Profile ID" (TilmanRoom,Heating_Input)  
            String TilmanRoom_Heating_AutomationProfileAlternateId "Heating Automation Alternate Profile ID" (TilmanRoom,Heating_Input)  

            */
            if(ActiveProfile == "Frostguard" || ActiveProfile == "Away" || ActiveProfile == "Present"){
                //The profile is one of the standard ones. Simply pass-through the value
                ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_AutomationProfileActiveId").postUpdate("" + ActiveProfile)
            }else{
                //Scan through the list and find the profile
                var detectedprofile = false
                ProfileList.members.forEach[ GroupItem d |
                    var ProfileName = d.members.findFirst[ p | p.name == "" + d.name + "_Name" ].state.toString
                    var ProfileType = d.members.findFirst[ p | p.name == "" + d.name + "_Type" ].state.toString
                    if(ProfileType == "Temperature" && ProfileName == ActiveProfile){
                        ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_AutomationProfileActiveId").postUpdate("" + d.name)
                        detectedprofile = true
                    }
                ]
                //Error Handling
                if(!detectedprofile){
                    logWarn("Heat Automation","Profile with the name of " + ActiveProfile + " does not exist in " + root_room )
                }
            }
            //Repeat for the pirmary profile
            var PrimaryProfile = "" + ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_AutomationProfilePrimary").state.toString
            if(PrimaryProfile == "Frostguard" || PrimaryProfile == "Away" || PrimaryProfile == "Present"){
                //The profile is one of the standard ones. Simply pass-through the value
                ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_AutomationProfilePrimaryId").postUpdate("" + PrimaryProfile)
            }else{
                //Scan through the list and find the profile
                var detectedprofile = false
                ProfileList.members.forEach[ GroupItem d |
                    var ProfileName = d.members.findFirst[ p | p.name == "" + d.name + "_Name" ].state.toString
                    var ProfileType = d.members.findFirst[ p | p.name == "" + d.name + "_Type" ].state.toString
                    if(ProfileType == "Temperature" && ProfileName == PrimaryProfile){
                        ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_AutomationProfilePrimaryId").postUpdate("" + d.name)
                        detectedprofile = true
                    }
                ]
                //Error Handling
                if(!detectedprofile){
                    logWarn("Heat Automation","Profile with the name of " + PrimaryProfile + " does not exist in " + root_room )
                }
            }
            //Repeat for the Alternate profile
            var AlternateProfile = "" + ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_AutomationProfileAlternate").state.toString
            if(AlternateProfile == "Frostguard" || AlternateProfile == "Away" || AlternateProfile == "Present"){
                //The profile is one of the standard ones. Simply pass-through the value
                ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_AutomationProfileAlternateId").postUpdate("" + AlternateProfile)
            }else{
                //Scan through the list and find the profile
                var detectedprofile = false
                ProfileList.members.forEach[ GroupItem d |
                    var ProfileName = d.members.findFirst[ p | p.name == "" + d.name + "_Name" ].state.toString
                    var ProfileType = d.members.findFirst[ p | p.name == "" + d.name + "_Type" ].state.toString
                    if(ProfileType == "Temperature" && ProfileName == AlternateProfile){
                        ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_AutomationProfileAlternateId").postUpdate("" + d.name)
                        detectedprofile = true
                    }
                ]
                //Error Handling
                if(!detectedprofile){
                    logWarn("Heat Automation","Profile with the name of " + AlternateProfile + " does not exist in " + root_room )
                }
            }


            if(debugmode){logInfo("Heat Automation","Mode is set to \"" + AutomationMode + "\" and the active profile is \"" + ActiveProfile + "\"")}

            // ====== STEP 2 ======
            // Fetch the current target temp from the profile and apply the offset

            if(debugmode){logInfo("Heat Automation", "Beginning Step 2, profile name is " + ProfileItemName + " with a fetch-value of " + CalculationFetched)}

            var TargetTemperature = 0.0

            val HeatingOffsetItem = ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_Offset")
            if(HeatingOffsetItem.state == NULL){
                HeatingOffsetItem.postUpdate(0.0)
            }
            var HeatingOffset = HeatingOffsetItem.state as DecimalType 

            if(ProfileItemName != "None"){
                if(debugmode){logInfo("Heat Automation","Reached profile_calc fetch")}
                TargetTemperature = CalculationFetched + HeatingOffset
            }else{
                if(ActiveProfile == "Frostguard"){
                    TargetTemperature = 12.0 + HeatingOffset
                }
                if(ActiveProfile == "Away"){
                    TargetTemperature = 15.0 + HeatingOffset
                }
                if(ActiveProfile == "Present"){
                    TargetTemperature = 21.0 + HeatingOffset
                }
            }
            
            //Actually send the target temperature to the item in question 
            ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_Target").postUpdate(TargetTemperature)

            if(debugmode){logInfo("Heat Automation","Calculated a value of " + TargetTemperature + " for this zone with ProfileItemName = " + ProfileItemName + ".")}

            if(debugmode){logInfo("Heat Automation","Room Complete!")}

            //logOutput
            if(root_room.length() < 8){
                if(ActiveProfile.length() < 7){
                    logInfo("Heat Automation","Computed " + root_room + " \t\t Active Profile: " + ActiveProfile + " \t\t Target " + TargetTemperature + " °C")
                }else{
                    logInfo("Heat Automation","Computed " + root_room + " \t\t Active Profile: " + ActiveProfile + " \t Target " + TargetTemperature + " °C")
                }
            }else{
                if(ActiveProfile.length() < 7){
                    logInfo("Heat Automation","Computed " + root_room + " \t Active Profile: " + ActiveProfile + " \t\t Target " + TargetTemperature + " °C")
                }else{
                    logInfo("Heat Automation","Computed " + root_room + " \t Active Profile: " + ActiveProfile + " \t Target " + TargetTemperature + " °C")
                }
            }
            

            // ====== STEP 3 ======
            // Send that value to all the radiators in that room
            Smart_Thermostat.members.forEach[ i |
                if(i.getGroupNames.contains(root_room) && i.getName.contains("Target_Temperature")){
                    //Change the thermostat only if it's set differently than the goal temp
                    if(i.state as DecimalType != TargetTemperature){
                        //item format is something like HKTTilman1_Target_Temperature
                        var thermoItem = Temperature_Targetable.members.findFirst[ i2 | i2.name == i.getName.split("_").get(0) + "_Control_Mode_Manu"]
                        //send the TargetTemperature to the individual thermostat
                        thermoItem.sendCommand(TargetTemperature as Number)
                        logInfo("Thermo Interval", "Thermostat \"" + thermoItem.name + "\" retargeted to " + TargetTemperature + ".")
                        Thread::sleep(rfDelay)
                    }
                }
            ]
            
        ]



    }
end

//This rule runs a localized heating zone update when 
rule "Heat Automation - Localized Update"
when
    Member of Heating_Input received command
then
    val root_room = triggeringItem.name.split('_').get(0)
    var root = Master_Heating_Switch.members.findFirst[ i | i.name == "" + triggeringItem.name.split('_').get(0) + "_Heating_Active" ]
    logInfo("Heat Automation","Running local Heat-Automation update in " + root_room)
    val debugmode = false
    //First, check for Null items and fix them if necessary
    if(root.state == NULL){
        logWarn("Null-Catcher", "Caught a NULL heating zone in " + root_room + "!")
        ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_Active").postUpdate(OFF)
        ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_Offset").postUpdate(0.0)
        ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_AutomationMode").postUpdate("Static")
        ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_AutomationProfilePrimary").postUpdate("Away")
        ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_AutomationProfileAlternate").postUpdate("Away")
        ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_WindowSuspension").postUpdate(OFF)
        //Note that the target and the active profile are not set, even in null cases. This is because they will later be calculated
        logWarn("Null-Catcher", "Catch Completed")
    }

    // ====== STEP 1 ======
    // Determine the active profile and error-check it
    if(debugmode){logInfo("Heat Automation", "Beginning Step 1 for " + root_room)}

    val AutomationModeItem = ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_AutomationMode") as GenericItem
    var AutomationMode = AutomationModeItem.state.toString
    val ActiveProfileItem = ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_AutomationProfileActive") as GenericItem

    //Quickly error-check the mode
    if(AutomationModeItem.state == NULL || !(AutomationMode == "Static" || AutomationMode == "Auto" || AutomationMode == "Smart")){
        logWarn("Heating Automation", "Mode error in " + AutomationModeItem.name + "! Setting it to Static from " + AutomationModeItem.state)
        AutomationModeItem.postUpdate("Static")
        AutomationMode = "Static"
    }

    var ActiveProfile = ""
    //Check to see if the window suspension is on. If it is, set the profile to Frostguard
    val WindowSuspensionItem = ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_WindowSuspension")
    var BoostMode = ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_BoostMode").state
    //Catch NULL
    if(BoostMode == NULL){
        BoostMode = OFF 
        ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_BoostMode").postUpdate(OFF)
    }

    val HeatActiveSwitch = ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_Active") as GenericItem
    if(HeatActiveSwitch.state == NULL){
        HeatActiveSwitch.postUpdate(OFF)
        
        ActiveProfile = "Frostguard"
        ActiveProfileItem.postUpdate(ActiveProfile)
    }

    if(HeatActiveSwitch.state == ON){
        if(WindowSuspensionItem.state == NULL){
            WindowSuspensionItem.postUpdate(OFF)
        }else if(WindowSuspensionItem.state == ON){
            //Window Suspension mode is on, override to Frostguard
            ActiveProfileItem.postUpdate("Frostguard")
            ActiveProfile = "Frostguard"
        }else{
            //Check to see if boost mode is active 
            if(BoostMode == ON){
                ActiveProfileItem.postUpdate("Present")
                ActiveProfile = "Present"
            }else{
                //window Suspension is not active
                if(AutomationMode == "Static"){
                    //Automation mode is Static, pass through the primary profile
                    ActiveProfile = ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_AutomationProfilePrimary").state.toString
                    ActiveProfileItem.postUpdate(ActiveProfile)
                }else if(AutomationMode == "Auto"){
                    //Determine if today is a workday or weekend day
                    if(now.getDayOfWeek.getValue >= 6){
                        //It's the weekend, move the Alternate profile into the Active one
                        ActiveProfile = ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_AutomationProfileAlternate").state.toString
                        ActiveProfileItem.postUpdate(ActiveProfile)
                    }else{
                        //It's a workday, move the Primary profile into the Active one
                        ActiveProfile = ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_AutomationProfilePrimary").state.toString
                        ActiveProfileItem.postUpdate(ActiveProfile)
                    }
                }else if(AutomationMode == "Smart"){
                    //The mode is set to smart
                    // TODO: Implement features, right now just immitates Static mode
                    ActiveProfile = ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_AutomationProfilePrimary").state.toString
                    ActiveProfileItem.postUpdate(ActiveProfile)
                }
            }
        }
    }else{
        ActiveProfile = "Frostguard"
        ActiveProfileItem.postUpdate(ActiveProfile)
    }

    //Check to see if the target profile is a real one
    var foundProfileInList = false 
    var ProfileItemName = ""
    var CalculationFetched = new DecimalType(0.0)

    //if(ActiveProfile == "Away" || ActiveProfile == "Frostguard" || ActiveProfile == "Present"){
    //    //We're good, no need to do anything. 
    //    ProfileItemName = "None"
    //    foundProfileInList = true
    //}else{
        //Search the list of profiles for one with a matching name and type
        ProfileList.members.forEach[ GroupItem d |
            //logInfo("Debug","Checking... " + d.name)

            var ProfileName = ScriptServiceUtil.getItemRegistry.getItem("" + d.name + "_Name" ).state.toString
            var ProfileType = ScriptServiceUtil.getItemRegistry.getItem("" + d.name + "_Type" ).state.toString
            if(ProfileType == "Temperature" && ProfileName.equals(ActiveProfile)){
                foundProfileInList = true
                ProfileItemName = d.name
                CalculationFetched = ScriptServiceUtil.getItemRegistry.getItem("" + d.name + "_Calculated" ).state as DecimalType
            }
        ]
    //}

    //Do Global vacation check
    if(Global_Vacation_Override.state == NULL){
        Global_Vacation_Override.postUpdate(OFF)
    }
    if(Global_Vacation_Override.state == ON){
        //On vacation, turn everything to frostguard
        ActiveProfile = "Frostguard"
        ActiveProfileItem.postUpdate(ActiveProfile)
    }

    //Do Local Heating Active Check
    if(HeatActiveSwitch.state == OFF){
        ActiveProfile = "Frostguard"
        ActiveProfileItem.postUpdate(ActiveProfile)
    }

    //Error found, handle it
    if(!foundProfileInList){
        logWarn("Heat Automation", "Profile \"" + ActiveProfile + "\" not found in list of profiles. Using \"Frostguard\" Profile as stand in")
        ActiveProfile = "Frostguard"
        ProfileItemName = "None"
        ActiveProfileItem.postUpdate(ActiveProfile)
    }

    if(ActiveProfile == "Frostguard" || ActiveProfile == "Away" || ActiveProfile == "Present"){
        //The profile is one of the standard ones. Simply pass-through the value
        ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_AutomationProfileActiveId").postUpdate("" + ActiveProfile)
    }else{
        //Scan through the list and find the profile
        var detectedprofile = false
        ProfileList.members.forEach[ GroupItem d |
            var ProfileName = d.members.findFirst[ p | p.name == "" + d.name + "_Name" ].state.toString
            var ProfileType = d.members.findFirst[ p | p.name == "" + d.name + "_Type" ].state.toString
            if(ProfileType == "Temperature" && ProfileName == ActiveProfile){
                ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_AutomationProfileActiveId").postUpdate("" + d.name)
                detectedprofile = true
            }
        ]
        //Error Handling
        if(!detectedprofile){
            logWarn("Heat Automation","Profile with the name of " + ActiveProfile + " does not exist in " + root_room )
        }
    }
    //Repeat for the pirmary profile
    var PrimaryProfile = "" + ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_AutomationProfilePrimary").state.toString
    if(PrimaryProfile == "Frostguard" || PrimaryProfile == "Away" || PrimaryProfile == "Present"){
        //The profile is one of the standard ones. Simply pass-through the value
        ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_AutomationProfilePrimaryId").postUpdate("" + PrimaryProfile)
    }else{
        //Scan through the list and find the profile
        var detectedprofile = false
        ProfileList.members.forEach[ GroupItem d |
            var ProfileName = d.members.findFirst[ p | p.name == "" + d.name + "_Name" ].state.toString
            var ProfileType = d.members.findFirst[ p | p.name == "" + d.name + "_Type" ].state.toString
            if(ProfileType == "Temperature" && ProfileName == PrimaryProfile){
                ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_AutomationProfilePrimaryId").postUpdate("" + d.name)
                detectedprofile = true
            }
        ]
        //Error Handling
        if(!detectedprofile){
            logWarn("Heat Automation","Profile with the name of " + PrimaryProfile + " does not exist in " + root_room )
        }
    }
    //Repeat for the Alternate profile
    var AlternateProfile = "" + ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_AutomationProfileAlternate").state.toString
    if(AlternateProfile == "Frostguard" || AlternateProfile == "Away" || AlternateProfile == "Present"){
        //The profile is one of the standard ones. Simply pass-through the value
        ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_AutomationProfileAlternateId").postUpdate("" + AlternateProfile)
    }else{
        //Scan through the list and find the profile
        var detectedprofile = false
        ProfileList.members.forEach[ GroupItem d |
            var ProfileName = d.members.findFirst[ p | p.name == "" + d.name + "_Name" ].state.toString
            var ProfileType = d.members.findFirst[ p | p.name == "" + d.name + "_Type" ].state.toString
            if(ProfileType == "Temperature" && ProfileName == AlternateProfile){
                ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_AutomationProfileAlternateId").postUpdate("" + d.name)
                detectedprofile = true
            }
        ]
        //Error Handling
        if(!detectedprofile){
            logWarn("Heat Automation","Profile with the name of " + AlternateProfile + " does not exist in " + root_room )
        }
    }


    if(debugmode){logInfo("Heat Automation","Mode is set to \"" + AutomationMode + "\" and the active profile is \"" + ActiveProfile + "\"")}

    // ====== STEP 2 ======
    // Fetch the current target temp from the profile and apply the offset

    if(debugmode){logInfo("Heat Automation", "Beginning Step 2, profile name is " + ProfileItemName + " with a fetch-value of " + CalculationFetched)}

    var TargetTemperature = 0.0

    val HeatingOffsetItem = ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_Offset")
    if(HeatingOffsetItem.state == NULL){
        HeatingOffsetItem.postUpdate(0.0)
    }
    var HeatingOffset = HeatingOffsetItem.state as DecimalType 

    if(ProfileItemName != "None"){
        if(debugmode){logInfo("Heat Automation","Reached profile_calc fetch")}
        TargetTemperature = CalculationFetched + HeatingOffset
    }else{
        if(ActiveProfile == "Frostguard"){
            TargetTemperature = 12.0 + HeatingOffset
        }
        if(ActiveProfile == "Away"){
            TargetTemperature = 15.0 + HeatingOffset
        }
        if(ActiveProfile == "Present"){
            TargetTemperature = 21.0 + HeatingOffset
        }
    }
    
    //Actually send the target temperature to the item in question 
    ScriptServiceUtil.getItemRegistry.getItem(root_room + "_Heating_Target").postUpdate(TargetTemperature)

    if(debugmode){logInfo("Heat Automation","Calculated a value of " + TargetTemperature + " for this zone with ProfileItemName = " + ProfileItemName + ".")}

    if(debugmode){logInfo("Heat Automation","Room Complete!")}

    //logOutput
    if(root_room.length() < 8){
        if(ActiveProfile.length() < 7){
            logInfo("Heat Automation","Computed " + root_room + " \t\t Active Profile: " + ActiveProfile + " \t\t Target " + TargetTemperature + " °C")
        }else{
            logInfo("Heat Automation","Computed " + root_room + " \t\t Active Profile: " + ActiveProfile + " \t Target " + TargetTemperature + " °C")
        }
    }else{
        if(ActiveProfile.length() < 7){
            logInfo("Heat Automation","Computed " + root_room + " \t Active Profile: " + ActiveProfile + " \t\t Target " + TargetTemperature + " °C")
        }else{
            logInfo("Heat Automation","Computed " + root_room + " \t Active Profile: " + ActiveProfile + " \t Target " + TargetTemperature + " °C")
        }
    }

    // ====== STEP 3 ======
    // Send that value to all the radiators in that room
    
    Smart_Thermostat.members.forEach[ i |
        if(i.getGroupNames.contains(root_room) && i.getName.contains("Target_Temperature")){
            //Change the thermostat only if it's set differently than the goal temp
            if(i.state as DecimalType != TargetTemperature){
                //item format is something like HKTTilman1_Target_Temperature
                var thermoItem = Temperature_Targetable.members.findFirst[ i2 | i2.name == i.getName.split("_").get(0) + "_Control_Mode_Manu"]
                //send the TargetTemperature to the individual thermostat
                thermoItem.sendCommand(TargetTemperature as Number)
                logInfo("Thermo Interval", "Thermostat \"" + thermoItem.name + "\" retargeted to " + TargetTemperature + ".")
                Thread::sleep(rfDelay)
            }
        }
    ]

end

rule "Heat Automation - Disable Boost Mode"
when
    Time cron "0 0 22 ? * * *"
then
    logInfo("Heat Automation","Boost deactivator rule exectued")

    Heating_Input.members.forEach[ GenericItem i |
        if(i.name.contains("BoostMode")){
            ScriptServiceUtil.getItemRegistry.getItem("" + i.name.split('_').get(0) + "_Heating_BoostMode").sendCommand(OFF)
        }
    ]

    logInfo("Heat Automation", "All Active Boosts deactivated!")
end
6 Likes

First of all I would like to thank you very much for your widgets and rules. I was able to set up the profile part withouth issues.
However I am struggling with the widgets here. The Profile Selection widget shows the right amount of buttons (I have 7 profiles with type Temperature and it shows 7 lines so the oh:repeater does read through the group right). However the buttons are empty and I cant alos click on them. Any idea why?

If the right amount of buttons are there, but no names, then it sounds to me like the “_Name” items are empty. For example, if you have these Virtual Items:

//BEGIN PROFILE BLOCK
Group Profile2 (ProfileList)
Group Profile2_val (Profile2)

String Profile2_Name        (Profile2) 
String Profile2_Command     (Profile2, ProfileCMD) 
String Profile2_Type        (Profile2, ProfileType) 
Switch Profile2_Interpolate (Profile2) 
Number Profile2_Calculated  (Profile2)
Number Profile2_MaxValue    (Profile2)
Number Profile2_MinValue    (Profile2)

Number Profile2_0 "00" (Profile2_val, ProfileSlider) 
Number Profile2_1 "01" (Profile2_val, ProfileSlider)
//continued..

They will show up in the list if Profile2_Type is “Temperature” and then it will populate the list with the matching values of the Profile2_Name items.

So, check if the <profile>_Name items are set to values. If they aren’t–if they are set to Null it will not work. In the other post where the profile rules are laid out, there are multiple rules which show examples of how these names can be set, for example in the rule rule "Profile Calculator - load defaults" there are lines that set the names like so: Profile1_Name.sendCommand("Standard Light Brightness").

Alternatively, you can open the profile_looper widget, and hit the gear to open a popup of profile_looper_popup which should let you set a name if none is set

Unfortunately that is not the issue, the items do have names. I did trigger the load defaults rule. Basically all items with Type “Temperature” have actually a name (Away, Present, Frostguard, Workday, Weekend, Hot Morning, Alternate Heat). Funnily if i change another profile to temperature it is going to add another line - so it identifies the number of Profile Items with “Temperature” right but it somehow doesnt populate the buttons with text

Okay, that’s unusual. What version of OpenHab are you using?

Here are some more things you can try:

  • Does it work if you open the widget temperature_control_v1_profilepopup in the Developer Widget editor?

  • Try this widget and see if the item states are displaying properly for debug purposes:

uid: profile_widget_debug
tags: []
props:
  parameters:
    - context: item
      description: Profile Item to be adjusted
      label: Item
      name: item
      required: false
      type: TEXT
  parameterGroups: []
timestamp: Dec 22, 2022, 4:26:57 PM
component: f7-card
config:
  title: "Select a profile:"
slots:
  default:
    - component: f7-list
      slots:
        default:
          - component: oh-repeater
            config:
              for: item
              fragment: true
              groupItem: ProfileList
              sourceType: itemsInGroup
            slots:
              default:
                - component: oh-list-item
                  config:
                    title: =" Profile:" + "  " +  loop.item.name + " |" + " Name Item Value:" + "  " + items[loop.item.name + "_Name" ].state + " |" + " Name Item Value:" + "  " + items[loop.item.name + "_Type" ].state
                      

I fixed it. The issue was with the the f7-list-item, i took this component away and directly made it the oh-button and now it works. I don’t exactly know why, maybe something to do with the Openhab version.

Find attached the code (In case someone else runs into the same issue)

uid: temperature_control_v1_profilepopup
tags: []
props:
  parameters:
    - context: item
      description: Profile Item to be adjusted
      label: Item
      name: item
      required: false
      type: TEXT
  parameterGroups: []
timestamp: Sep 15, 2023, 4:52:03 PM
component: f7-card
config:
  title: "Select a profile:"
slots:
  default:
    - component: f7-list
      slots:
        default:
          - component: oh-repeater
            config:
              filter: items[loop.item.name + '_Type'].state == 'Temperature'
              for: item
              fragment: true
              groupItem: ProfileList
              sourceType: itemsInGroup
            slots:
              default:
                 - component: oh-button
                   config:
                          action: command
                          actionCommand: =items[loop.item.name + '_Name'].state
                          actionItem: =props.item
                          fill: "=(items[props.item].state == items[loop.item.name + '_Name'].state ? true : false)"
                          style:
                            width: 100%
                          text: =items[loop.item.name + '_Name'].state

1 Like

Ah, great! Very likely that this is something to do with OH versions, considering the code is almost a year old and I habitually avoid upgrading unless necessary because upgrades always break something.

I have installed the profile widget/system and can view and change the profiles etc.

I am on openHAB 4.0.4.

The heat control widget /system generates the following for me.
grafik

Does anyone have an idea what the problem could be?

Probably some change to styling in OH4. Does the widget look correct in the Widget Development tool?

Yes, the widgets are displayed correctly in the widget development tool.
Can I also use them directly?
For this, something must surely be deleted in the rules so that they are not used in the MainUI.

As far as I remember when i tried to set it up, I did have a lot of issues with the widgets and ended up recreating them based on Marmoset_Threat’s Widgets (I still used a lot of his awesome work as basis, thats also the reason I didnt want to share this here as it looks like i created something on my own when I all actually did was using the presets and tweaked them).

It looks like this for me:

Basic Heating Set-Up:
image

If you click on “Einstellungen Heizprofile” it will open up the profile list.
image

You can then click on the profiles to modify them
image

If you click on any of the rooms it will open up the room specific settings:

image
image

I can share this with you but I also did end up rewriting all the code in blockly cause I didnt get it the code from Marmoset_Threat to work and I also wanted to learn how this works and set it up the way I wanted…

Howver as stated I dont want to just post this here - only if it is ok for Marmoset_Threat

Sounds awesome, go ahead and share!

Ok with the blessing here it comes and some details of what I added or changed:

I just copied the “Code” Part from the Blockly rules (minus that blockSource stuff as it made everything very messy), I hope this works for everyone - otherwise if anyone can tell me how to export it in a better way - please do tell and i’ll update my post.
Also need to preface: I am not an programmer or IT guy. I do this on my free time, so it might be not so good code or contain bugs. So far it works for us though.

I will separate this into 2 parts as I also reworked the whole profile list rules.

First part is the Adjusted Profile List. I dont use anything else then heating, therefore I only use °C and did skip all the part with other profile types.

Items
Group ProfileList "List of all available profiles"
Group Profile_Reset 
Group ProfileList
Switch ProfileMasterReset

//BEGIN PROFILE BLOCK
Group Profile1 (ProfileList)
Group Profile1_val (Profile1)

String Profile1_Name (Profile1) 
Number Profile1_Calculated  (Profile1)
Number Profile1_MinValue (Profile1)
Number Profile1_MaxValue (Profile1)
Switch Profile1_Reset (Profile1, Profile_Reset)

Number Profile1_0 "00" (Profile1_val) 
Number Profile1_1 "01" (Profile1_val)
Number Profile1_2 "02" (Profile1_val)
Number Profile1_3 "03" (Profile1_val)
Number Profile1_4 "04" (Profile1_val)
Number Profile1_5 "05" (Profile1_val)
Number Profile1_6 "06" (Profile1_val)
Number Profile1_7 "07" (Profile1_val)
Number Profile1_8 "08" (Profile1_val)
Number Profile1_9 "09" (Profile1_val)
Number Profile1_10 "10" (Profile1_val)
Number Profile1_11 "11" (Profile1_val)
Number Profile1_12 "12" (Profile1_val)
Number Profile1_13 "13" (Profile1_val)
Number Profile1_14 "14" (Profile1_val)
Number Profile1_15 "15" (Profile1_val)
Number Profile1_16 "16" (Profile1_val)
Number Profile1_17 "17" (Profile1_val)
Number Profile1_18 "18" (Profile1_val)
Number Profile1_19 "19" (Profile1_val)
Number Profile1_20 "20" (Profile1_val)
Number Profile1_21 "21" (Profile1_val)
Number Profile1_22 "22" (Profile1_val)
Number Profile1_23 "23" (Profile1_val)

//BEGIN PROFILE BLOCK
Group Profile2 (ProfileList)
Group Profile2_val (Profile2)

String Profile2_Name (Profile2) 
Number Profile2_Calculated  (Profile2)
Number Profile2_MinValue (Profile2)
Number Profile2_MaxValue (Profile2)
Switch Profile2_Reset (Profile2, Profile_Reset)

Number Profile2_0 "00" (Profile2_val) 
Number Profile2_1 "01" (Profile2_val)
Number Profile2_2 "02" (Profile2_val)
Number Profile2_3 "03" (Profile2_val)
Number Profile2_4 "04" (Profile2_val)
Number Profile2_5 "05" (Profile2_val)
Number Profile2_6 "06" (Profile2_val)
Number Profile2_7 "07" (Profile2_val)
Number Profile2_8 "08" (Profile2_val)
Number Profile2_9 "09" (Profile2_val)
Number Profile2_10 "10" (Profile2_val)
Number Profile2_11 "11" (Profile2_val)
Number Profile2_12 "12" (Profile2_val)
Number Profile2_13 "13" (Profile2_val)
Number Profile2_14 "14" (Profile2_val)
Number Profile2_15 "15" (Profile2_val)
Number Profile2_16 "16" (Profile2_val)
Number Profile2_17 "17" (Profile2_val)
Number Profile2_18 "18" (Profile2_val)
Number Profile2_19 "19" (Profile2_val)
Number Profile2_20 "20" (Profile2_val)
Number Profile2_21 "21" (Profile2_val)
Number Profile2_22 "22" (Profile2_val)
Number Profile2_23 "23" (Profile2_val)

//BEGIN PROFILE BLOCK
Group Profile3 (ProfileList)
Group Profile3_val (Profile3)

String Profile3_Name (Profile3) 
Number Profile3_Calculated  (Profile3)
Number Profile3_MinValue (Profile3)
Number Profile3_MaxValue (Profile3)
Switch Profile3_Reset (Profile3, Profile_Reset)

Number Profile3_0 "00" (Profile3_val) 
Number Profile3_1 "01" (Profile3_val)
Number Profile3_2 "02" (Profile3_val)
Number Profile3_3 "03" (Profile3_val)
Number Profile3_4 "04" (Profile3_val)
Number Profile3_5 "05" (Profile3_val)
Number Profile3_6 "06" (Profile3_val)
Number Profile3_7 "07" (Profile3_val)
Number Profile3_8 "08" (Profile3_val)
Number Profile3_9 "09" (Profile3_val)
Number Profile3_10 "10" (Profile3_val)
Number Profile3_11 "11" (Profile3_val)
Number Profile3_12 "12" (Profile3_val)
Number Profile3_13 "13" (Profile3_val)
Number Profile3_14 "14" (Profile3_val)
Number Profile3_15 "15" (Profile3_val)
Number Profile3_16 "16" (Profile3_val)
Number Profile3_17 "17" (Profile3_val)
Number Profile3_18 "18" (Profile3_val)
Number Profile3_19 "19" (Profile3_val)
Number Profile3_20 "20" (Profile3_val)
Number Profile3_21 "21" (Profile3_val)
Number Profile3_22 "22" (Profile3_val)
Number Profile3_23 "23" (Profile3_val)

//BEGIN PROFILE BLOCK
Group Profile4 (ProfileList)
Group Profile4_val (Profile4)

String Profile4_Name (Profile4) 
Number Profile4_Calculated  (Profile4)
Number Profile4_MinValue (Profile4)
Number Profile4_MaxValue (Profile4)
Switch Profile4_Reset (Profile4, Profile_Reset)

Number Profile4_0 "00" (Profile4_val) 
Number Profile4_1 "01" (Profile4_val)
Number Profile4_2 "02" (Profile4_val)
Number Profile4_3 "03" (Profile4_val)
Number Profile4_4 "04" (Profile4_val)
Number Profile4_5 "05" (Profile4_val)
Number Profile4_6 "06" (Profile4_val)
Number Profile4_7 "07" (Profile4_val)
Number Profile4_8 "08" (Profile4_val)
Number Profile4_9 "09" (Profile4_val)
Number Profile4_10 "10" (Profile4_val)
Number Profile4_11 "11" (Profile4_val)
Number Profile4_12 "12" (Profile4_val)
Number Profile4_13 "13" (Profile4_val)
Number Profile4_14 "14" (Profile4_val)
Number Profile4_15 "15" (Profile4_val)
Number Profile4_16 "16" (Profile4_val)
Number Profile4_17 "17" (Profile4_val)
Number Profile4_18 "18" (Profile4_val)
Number Profile4_19 "19" (Profile4_val)
Number Profile4_20 "20" (Profile4_val)
Number Profile4_21 "21" (Profile4_val)
Number Profile4_22 "22" (Profile4_val)
Number Profile4_23 "23" (Profile4_val)

//BEGIN PROFILE BLOCK
Group Profile5 (ProfileList)
Group Profile5_val (Profile5)

String Profile5_Name (Profile5) 
Number Profile5_Calculated  (Profile5)
Number Profile5_MinValue (Profile5)
Number Profile5_MaxValue (Profile5)
Switch Profile5_Reset (Profile5, Profile_Reset)

Number Profile5_0 "00" (Profile5_val) 
Number Profile5_1 "01" (Profile5_val)
Number Profile5_2 "02" (Profile5_val)
Number Profile5_3 "03" (Profile5_val)
Number Profile5_4 "04" (Profile5_val)
Number Profile5_5 "05" (Profile5_val)
Number Profile5_6 "06" (Profile5_val)
Number Profile5_7 "07" (Profile5_val)
Number Profile5_8 "08" (Profile5_val)
Number Profile5_9 "09" (Profile5_val)
Number Profile5_10 "10" (Profile5_val)
Number Profile5_11 "11" (Profile5_val)
Number Profile5_12 "12" (Profile5_val)
Number Profile5_13 "13" (Profile5_val)
Number Profile5_14 "14" (Profile5_val)
Number Profile5_15 "15" (Profile5_val)
Number Profile5_16 "16" (Profile5_val)
Number Profile5_17 "17" (Profile5_val)
Number Profile5_18 "18" (Profile5_val)
Number Profile5_19 "19" (Profile5_val)
Number Profile5_20 "20" (Profile5_val)
Number Profile5_21 "21" (Profile5_val)
Number Profile5_22 "22" (Profile5_val)
Number Profile5_23 "23" (Profile5_val)

Rule: Profile Masterreset
configuration: {}
triggers:
  - id: "1"
    configuration:
      command: ON
      itemName: ProfileMasterReset
    type: core.ItemCommandTrigger
conditions: []
actions:
  - inputs: {}
    id: "2"
    configuration:
         type: application/javascript
      script: >
        var items_in_reset_group, profile_reset, children_in_reset_group;


        var thread = Java.type('java.lang.Thread')



        var items_in_reset_group_list = items.getItem('Profile_Reset').members;

        for (var items_in_reset_group_index in items_in_reset_group_list) {
          items_in_reset_group = items_in_reset_group_list[items_in_reset_group_index];
          profile_reset = items_in_reset_group.name;
          items.getItem(profile_reset).sendCommand('ON');
        }

        thread.sleep(5000);

        items.getItem('Abwesend_Name').sendCommand('Abwesend');

        var children_in_reset_group_list = items.getItem('Abwesend_val').members;

        for (var children_in_reset_group_index in children_in_reset_group_list) {
          children_in_reset_group = children_in_reset_group_list[children_in_reset_group_index];
          children_in_reset_group.sendCommand(15);
        }

        thread.sleep(3000);

        items.getItem('Aus_Name').sendCommand('Aus');

        var children_in_reset_group_list2 = items.getItem('Aus_val').members;

        for (var children_in_reset_group_index2 in children_in_reset_group_list2) {
          children_in_reset_group = children_in_reset_group_list2[children_in_reset_group_index2];
          children_in_reset_group.sendCommand(5);
        }

        items.getItem('Boost_Name').sendCommand('Boost');

        thread.sleep(3000);

        var children_in_reset_group_list3 = items.getItem('Boost_val').members;

        for (var children_in_reset_group_index3 in children_in_reset_group_list3) {
          children_in_reset_group = children_in_reset_group_list3[children_in_reset_group_index3];
          children_in_reset_group.sendCommand(30);
        }

        items.getItem('ProfileMasterReset').sendCommand('OFF');
    type: script.ScriptAction

Rule: Profile Reset
configuration: {}
triggers:
  - id: "1"
    configuration:
      command: ON
      groupName: Profile_Reset
    type: core.GroupCommandTrigger
conditions: []
actions:
  - inputs: {}
    id: "2"
    configuration:

      type: application/javascript
      script: >
        var profile_to_reset, Profile_Identificator, Profile_Group,
        children_in_reset_group, Profile_Name, Profile_MinValue,
        Profile_MaxValue, Profile_Calculated;



        profile_to_reset = event.itemName;

        Profile_Identificator = profile_to_reset.slice(0, (profile_to_reset.indexOf('_') + 1) - 1);

        console.info(('Profile identified: ' + String(Profile_Identificator)));

        Profile_Group = [Profile_Identificator,'_','val'].join('');

        var children_in_reset_group_list = items.getItem(Profile_Group).members;

        for (var children_in_reset_group_index in children_in_reset_group_list) {
          children_in_reset_group = children_in_reset_group_list[children_in_reset_group_index];
          children_in_reset_group.sendCommand(5);
        }

        Profile_Name = [Profile_Identificator,'_','Name'].join('');

        items.getItem(Profile_Name).sendCommand(Profile_Identificator);

        Profile_MinValue = [Profile_Identificator,'_','MinValue'].join('');

        items.getItem(Profile_MinValue).sendCommand(0);

        Profile_MaxValue = [Profile_Identificator,'_','MinValue'].join('');

        items.getItem(Profile_MaxValue).sendCommand(0);

        Profile_Calculated = [Profile_Identificator,'_','Calculated'].join('');

        items.getItem(Profile_Calculated).sendCommand(0);

        items.getItem(profile_to_reset).sendCommand('OFF');
    type: script.ScriptAction

Rule: Calculate Current Value
configuration: {}
triggers:
  - id: "1"
    configuration:
      cronExpression: 0 0/1 * * * ? *
    type: timer.GenericCronTrigger
conditions: []
actions:
  - inputs: {}
    id: "2"
    configuration:
      type: application/javascript
      script: >
        var aktuelles_intervall, available_profiles,
        identifikator_val_gruppe_aktuelle_stunde, profil_variable_target_wert,
        wert_aktuelles_intervall_gruppe;



        aktuelles_intervall = ((time.ZonedDateTime.now()).hour());

        var available_profiles_list = items.getItem('ProfileList').members;

        for (var available_profiles_index in available_profiles_list) {
          available_profiles = available_profiles_list[available_profiles_index];
          available_profiles = available_profiles.name;
          identifikator_val_gruppe_aktuelle_stunde = [available_profiles,'_',aktuelles_intervall].join('');
          profil_variable_target_wert = [available_profiles,'_','Calculated'].join('');
          wert_aktuelles_intervall_gruppe = items.getItem(identifikator_val_gruppe_aktuelle_stunde).state;
          items.getItem(profil_variable_target_wert).sendCommand(wert_aktuelles_intervall_gruppe);
        }
    type: script.ScriptAction

Widget: Profile Picker
uid: Auswahl_Heizungsprofile
tags: []
props:
  parameters: []
  parameterGroups: []
timestamp: Sep 19, 2023, 12:27:07 PM
component: f7-card
config:
  title: Heizungsprofile
slots:
  default:
    - component: f7-list
      slots:
        default:
          - component: oh-repeater
            config:
              for: item
              fragment: true
              groupItem: ProfileList
              sourceType: itemsInGroup
            slots:
              default:
                - component: oh-button
                  config:
                    action: command
                    actionCommand: =items[loop.item.name + '_Name'].state
                    actionItem: =props.item
                    fill: "=(items[props.item].state == items[loop.item.name + '_Name'].state ? true : false)"
                    text: =items[loop.item.name + '_Name'].state

Widget: Profile Settings
uid: Heizeinstellungen_Profil_Popup
tags: []
props:
  parameters:
    - context: item
      description: Zu änderndes Profil
      label: Item
      name: profile
      required: false
      type: TEXT
  parameterGroups: []
timestamp: Sep 16, 2023, 6:25:40 PM
component: f7-card
config:
  title: ="Profileinstellungen für " + items[props.profile + "_Name"].state
slots:
  default:
    - {}
    - component: f7-card-content
      slots:
        default:
          - component: f7-row
            config:
              style:
                height: 3em
            slots:
              default:
                - component: f7-col
                  config:
                    width: 25
                  slots:
                    default:
                      - component: Label
                        config:
                          text: Profilname
                - component: f7-col
                  config:
                    width: 75
                  slots:
                    default:
                      - component: oh-input
                        config:
                          item: =props.profile + '_Name'
                          name: Name
                          outline: true
                          placeholder: = items[props.profile + '_Name'].state
                          sendButton: true
                          type: text
          - component: f7-row
            slots:
              default:
                - component: f7-col
                  config: {}
                  slots:
                    default:
                      - component: oh-button
                        config:
                          action: toggle
                          actionAltCommand: On
                          actionCommand: ON
                          actionItem: =props.profile + '_Reset'
                          large: true
                          outline: true
                          style:
                            margin-top: 1em
                          text: Profilreset?

Widget: List of Profiles
uid: Liste_Heizungsprofile
tags: []
props:
  parameters: []
  parameterGroups: []
timestamp: Sep 16, 2023, 10:47:43 PM
component: f7-card
config:
  title: Heizungsprofile
slots:
  default:
    - component: f7-list
      slots:
        default:
          - component: oh-repeater
            config:
              for: item
              fragment: true
              groupItem: ProfileList
              sourceType: itemsInGroup
            slots:
              default:
                - component: oh-button
                  config:
                    action: popup
                    actionModal: widget:Heizungsprofil_Einstellungen
                    actionModalConfig:
                      profile: =loop.item.name
                    text: =items[loop.item.name + '_Name'].state

Now for the Automated Heating part:

I did add groups for open windows per room (Deactivates the heating in the room if any window of that room is open), as well a group checking wether me or my wife or both are away (For example: If my wife is away, the heating in her office will be automatically set to away mode but in all the “common” rooms it will stay on. If we both are away then all the heatings will be put to away mode)
I also did add a group Boostmode that helps me reset the boost mode 30 min after it being activated (to make sure boostmode is not running endless) and a group called override which helps me do the same for the “Temperature Calibration” setting (So if someone manually adjusts the temperature, it will reset it back to the profile value after 3 hours)

Items
//Heating Control Items (Virtual)
//Global Groups
Switch Global_Vacation_Override "Heizung aus / Sommermodus / Away Modus"
Group windowsensor
Group:Number:COUNT(ON) boostmode
Switch Trigger_HeatAutomation_Script
Group away
Group override


//Heat Specific Groups
Group Heating_Input
Group Master_Heating_Switch 

//Heating items for Wohnzimmer

Group Heating_Wohnzimmer (wohnzimmer)

Switch wohnzimmer_Heating_Active "Heating Active" (Master_Heating_Switch, Heating_Wohnzimmer, Heating_Input)  
Number wohnzimmer_Heating_Offset "Heating Offset" (Heating_Wohnzimmer, Heating_Input, override)  
Number wohnzimmer_Heating_Target "Heating Target" (Heating_Wohnzimmer, Heating_Input)  
String wohnzimmer_Heating_AutomationMode "Heating Automation Mode" (Heating_Wohnzimmer, Heating_Input)  
String wohnzimmer_Heating_AutomationProfilePrimary "Heating Automation Primary Profile" (Heating_Wohnzimmer, Heating_Input)  
String wohnzimmer_Heating_AutomationProfileAlternate "Heating Automation Alternate Profile" (Heating_Wohnzimmer, Heating_Input)  
String wohnzimmer_Heating_AutomationProfileActive "Heating Automation Active Profile" (Heating_Wohnzimmer, Heating_Input)  
String wohnzimmer_Heating_AutomationProfileActiveID "Heating Automation Active Profile ID" (Heating_Wohnzimmer)  
Switch wohnzimmer_Heating_BoostMode "Heating Boost Mode" (Heating_Wohnzimmer, boostmode, Heating_Input)  
Switch wohnzimmer_Heating_WindowSuspension "Heating Window Suspension" (Heating_Wohnzimmer, Heating_Input)
Group:Number:AVG wohnzimmer_Temperature "Average Temperature" <temperature> (Heating_Wohnzimmer, Heating_Input)  
Group:Number:COUNT(false) wohnzimmer_windowsensor "Anzahl offene Fenster" (Heating_Wohnzimmer, windowsensor, Heating_Input) 
Group:Number:COUNT(OFF) wohnzimmer_away "Person1 / Person2 Away" (Heating_Wohnzimmer, away, Heating_Input)
Switch wohnzimmer_Heating_AwaySwitch "Wohnzimmer Away Switch" (Heating_Wohnzimmer, Heating_Input)

I do have Zigbee Thermostats that are not very smart (they stop heating once the local temperature measured on the thermostat is higher than the setpoint - which happens pretty fast as the thermostat temperature is directly next to the heating and therefore quickly higher than room temperature). In order to counter that, I added a rule that checks if the room temperature matches the target temperature and than variablly adjusts the temperature offset so that room temperature = thermostat temperature.

Rule: Regular Heating Check
configuration: {}
triggers:
  - id: "1"
    configuration:
      cronExpression: 0 0/10 * * * ? *
    type: timer.GenericCronTrigger
  - id: "3"
    configuration:
      command: ON
      itemName: Trigger_HeatAutomation_Script
    type: core.ItemCommandTrigger
  - id: "4"
    configuration:
      itemName: Global_Vacation_Override
    type: core.ItemStateChangeTrigger
conditions: []
actions:
  - inputs: {}
    id: "2"
    configuration:
       type: application/javascript
      script: >
        var Kindersicherung, member_of_master_heating_switch_group, root_room,
        Heating_Active, Heating_AutomationMode,
        Heating_AutomationProfilePrimary, Heating_AutomationProfileAlternate,
        Heating_Offset, Heating_WindowSuspension,
        Heating_AutomationProfileActive, Heating_BoostMode, Heating_AwaySwitch,
        Heating_AutomationProfileActiveID, TemperatureCalibration,
        CurrentSetPoint, SystemMode, Temperature, ThermostatTemperature,
        AutomationMode, member_of_profile_list, ActiveProfile, Profile_Group,
        Name_of_Profile, Target_Temperature, profile_calculated, Heating_Target,
        Pulled_Target_Temperature, Heating_Offset_Value, CurrentTemperatureRoom,
        UpdatedCurrentTemperature_Room, Weekend, TemperatureOffset,
        TemperatureOffsetRounded;


        var thread = Java.type('java.lang.Thread')



        items.getItem('Trigger_HeatAutomation_Script').sendCommand('OFF');

        thread.sleep(2000);

        var member_of_master_heating_switch_group_list = items.getItem('Master_Heating_Switch').members;

        for (var member_of_master_heating_switch_group_index in member_of_master_heating_switch_group_list) {
          member_of_master_heating_switch_group = member_of_master_heating_switch_group_list[member_of_master_heating_switch_group_index];
          root_room = member_of_master_heating_switch_group.name.slice(0, (member_of_master_heating_switch_group.name.indexOf('_') + 1) - 1);
          Heating_Active = String(root_room) + '_Heating_Active';
          Heating_AutomationMode = String(root_room) + '_Heating_AutomationMode';
          Heating_AutomationProfilePrimary = String(root_room) + '_Heating_AutomationProfilePrimary';
          Heating_AutomationProfileAlternate = String(root_room) + '_Heating_AutomationProfileAlternate';
          Heating_Offset = String(root_room) + '_Heating_Offset';
          Heating_WindowSuspension = String(root_room) + '_Heating_WindowSuspension';
          Heating_AutomationProfileActive = String(root_room) + '_Heating_AutomationProfileActive';
          Heating_BoostMode = String(root_room) + '_Heating_BoostMode';
          Heating_AwaySwitch = String(root_room) + '_Heating_AwaySwitch';
          Heating_AutomationProfileActiveID = String(root_room) + '_Heating_AutomationProfileActiveID';
          TemperatureCalibration = String(root_room) + '_thermostat_Temperature_Calibration';
          Kindersicherung = String(root_room) + '_thermostat_Kindersicherung';
          CurrentSetPoint = String(root_room) + '_thermostat_Current_Setpoint';
          SystemMode = String(root_room) + '_thermostat_System_Mode';
          Temperature = String(root_room) + '_Temperature';
          ThermostatTemperature = String(root_room) + '_thermostat_Local_Temperature';
          if (items.getItem(Heating_Active).state == 'NULL') {
            items.getItem(Heating_Active).postUpdate('OFF');
          }
          if (items.getItem(Heating_AutomationMode).state == 'NULL') {
            items.getItem(Heating_AutomationMode).postUpdate('Static');
          }
          if (items.getItem(Heating_AutomationProfilePrimary).state == 'NULL') {
            items.getItem(Heating_AutomationProfilePrimary).postUpdate('Abwesend');
            if (items.getItem(Heating_AutomationProfileAlternate).state == 'NULL') {
              items.getItem(Heating_AutomationProfileAlternate).postUpdate('Abwesend');
            }
          }
          if (items.getItem(Heating_Offset).state == 'NULL') {
            items.getItem(Heating_Offset).postUpdate(0);
          }
          if (items.getItem(Heating_WindowSuspension).state == 'NULL') {
            items.getItem(Heating_WindowSuspension).postUpdate('OFF');
          }
          if (items.getItem(Heating_BoostMode).state == 'NULL') {
            items.getItem(Heating_BoostMode).postUpdate('OFF');
          }
          AutomationMode = items.getItem(Heating_AutomationMode).state;
          if (AutomationMode != 'Auto' && AutomationMode != 'Static') {
            items.getItem(Heating_AutomationMode).postUpdate('Static');
          } else {
          }
          if (items.getItem('Global_Vacation_Override').state == 'ON') {
            items.getItem(Heating_AutomationProfileActive).postUpdate('Aus');
            ActiveProfile = items.getItem(Heating_AutomationProfileActive).state;
          } else {
            if (items.getItem(Heating_Active).state == 'ON') {
              if (items.getItem(Heating_WindowSuspension).state == 'ON') {
                items.getItem(Heating_AutomationProfileActive).postUpdate('Aus');
                ActiveProfile = items.getItem(Heating_AutomationProfileActive).state;
              } else {
                if (items.getItem(Heating_AwaySwitch).state == 'ON') {
                  items.getItem(Heating_AutomationProfileActive).postUpdate('Abwesend');
                  ActiveProfile = items.getItem(Heating_AutomationProfileActive).state;
                } else {
                  if (items.getItem(Heating_BoostMode).state == 'ON') {
                    items.getItem(Heating_AutomationProfileActive).postUpdate('Boost');
                    ActiveProfile = items.getItem(Heating_AutomationProfileActive).state;
                  } else {
                    if (items.getItem(Heating_AutomationMode).state == 'Static') {
                      items.getItem(Heating_AutomationProfileActive).postUpdate(items.getItem(Heating_AutomationProfilePrimary).state);
                      ActiveProfile = items.getItem(Heating_AutomationProfilePrimary).state;
                    } else {
                      if (items.getItem(Heating_AutomationMode).state == 'Auto') {
                        Weekend = ((time.ZonedDateTime.now()).dayOfWeek().value());
                        if (Weekend < 6) {
                          items.getItem(Heating_AutomationProfileActive).postUpdate(items.getItem(Heating_AutomationProfilePrimary).state);
                          ActiveProfile = items.getItem(Heating_AutomationProfilePrimary).state;
                        } else {
                          items.getItem(Heating_AutomationProfileActive).postUpdate(items.getItem(Heating_AutomationProfileAlternate).state);
                          ActiveProfile = items.getItem(Heating_AutomationProfileAlternate).state;
                        }
                      }
                    }
                  }
                }
              }
            } else {
              items.getItem(Heating_AutomationProfileActive).postUpdate('Aus');
              ActiveProfile = items.getItem(Heating_AutomationProfileActive).state;
            }
          }
          var member_of_profile_list_list = items.getItem('ProfileList').members;
          for (var member_of_profile_list_index in member_of_profile_list_list) {
            member_of_profile_list = member_of_profile_list_list[member_of_profile_list_index];
            Profile_Group = [member_of_profile_list.name,'_','Name'].join('');
            Name_of_Profile = items.getItem(Profile_Group).state;
            if (Name_of_Profile == ActiveProfile) {
              items.getItem(Heating_AutomationProfileActiveID).sendCommand(member_of_profile_list.name);
              profile_calculated = [member_of_profile_list.name,'_','Calculated'].join('');
              Heating_Target = String(root_room) + '_Heating_Target';
              Pulled_Target_Temperature = items.getItem(profile_calculated).state;
              Heating_Offset = String(root_room) + '_Heating_Offset';
              Heating_Offset_Value = items.getItem(Heating_Offset).state;
              Target_Temperature = (Quantity(Pulled_Target_Temperature).add(Quantity(Heating_Offset_Value)));
              items.getItem(Heating_Target).sendCommand(Target_Temperature);
            }
          }
          if (items.getItem(Kindersicherung).state == 'UNLOCK') {
            items.getItem(Kindersicherung).sendCommand('LOCK');
          } else if (items.getItem(Kindersicherung).state == 'LOCK') {
          }
          if (ActiveProfile == 'Aus') {
            if (items.getItem(SystemMode).state == 'heat' || items.getItem(SystemMode).state == 'auto') {
              items.getItem(SystemMode).sendCommand('off');
            }
          } else if (ActiveProfile != 'Aus') {
            if (items.getItem(SystemMode).state != 'heat') {
              items.getItem(SystemMode).sendCommand('heat');
            }
          }
          console.info(('Check if Current SetPoint of Thermostat matches the Target Temperature and change it if needed for room: ' + String(root_room)));
          console.info(('Current Setpoint is: ' + String(items.getItem(CurrentSetPoint).state)));
          console.info(('Current Target Temp is ' + String(Target_Temperature)));
          if (items.getItem(CurrentSetPoint).state == Target_Temperature) {
            console.info('Thermostat Temperature already set to Target Temperature - do nothing');
          } else {
            console.info('Thermostat Temperature not matching Target Temperature - Change Temperature');
            items.getItem(CurrentSetPoint).sendCommand(Target_Temperature);
          }
          console.info('------------------------------------------------------------------------');
          console.info('Start Thermostat Calibration Script');
          CurrentTemperatureRoom = Quantity(items.getItem(Temperature).state);
          console.info('Adding 0.5°C to CurrentTemperature for less sensivity');
          UpdatedCurrentTemperature_Room = (Quantity(CurrentTemperatureRoom).add(Quantity('0.5')));
          console.info((['Actual Room Temperature : ',CurrentTemperatureRoom,' Updated Room Temperature: ',UpdatedCurrentTemperature_Room].join('')));
          if (UpdatedCurrentTemperature_Room < Target_Temperature) {
            console.info('Room Temperature Lower than Target_Temperature modify Temperature Offset to adjust it');
            console.info('Calculate needed Offset to match Sensor Temperature and Thermostat Temperature');
            console.info('Check if Thermostat Temp is under Room Temp');
            console.info('Room Temperature is: ');
            console.info(Quantity(items.getItem(Temperature).state));
            console.info('Thermostat Temperature is: ');
            console.info(Quantity(items.getItem(ThermostatTemperature).state));
            TemperatureOffset = (Quantity(items.getItem(Temperature).state).subtract(Quantity(items.getItem(ThermostatTemperature).state)));
            TemperatureOffsetRounded = Math.round(TemperatureOffset);
            if (TemperatureOffset < -6) {
              TemperatureOffsetRounded = -6;
            } else if (TemperatureOffset > 6) {
              TemperatureOffsetRounded = 6;
            }
            items.getItem(TemperatureCalibration).sendCommand(TemperatureOffsetRounded);
            console.info(('Offset Calculated and updated to: ' + String(TemperatureOffsetRounded)));
            console.info('Room Temperature Higher than Target temperature, reset Temperature Calibration');
            console.info(('Current Temperature Calibration is:' + String(items.getItem(TemperatureCalibration).state)));
          } else if (UpdatedCurrentTemperature_Room > CurrentSetPoint && items.getItem(TemperatureCalibration).state != '0') {
            console.info('Room Temperature Higher than Target temperature, reset Temperature Calibration');
            items.getItem(TemperatureCalibration).sendCommand('0');
          } else {
            console.info('Temperature already properly calibrated - do nothing');
          }
          console.info('----------------------------------------------------------------');
        }


        Kindersicherung;


        Kindersicherung;
    type: script.ScriptAction

Rule: Heating Input Trigger
configuration: {}
triggers:
  - id: "1"
    configuration:
      groupName: Heating_Input
    type: core.GroupStateChangeTrigger
conditions: []
actions:
  - inputs: {}
    id: "2"
    configuration:
      type: application/javascript
      script: >
        var root_room, Triggering_item, Heating_Active, Heating_AutomationMode,
        Heating_AutomationProfilePrimary, Heating_AutomationProfileAlternate,
        Heating_Offset, Heating_WindowSuspension,
        Heating_AutomationProfileActive, Heating_BoostMode, Heating_AwaySwitch,
        Heating_AutomationProfileActiveID, TemperatureCalibration,
        Kindersicherung, CurrentSetPoint, SystemMode, Temperature,
        ThermostatTemperature, AutomationMode, member_of_profile_list,
        ActiveProfile, Profile_Group, Name_of_Profile, Target_Temperature,
        profile_calculated, Heating_Target, CurrentTemperatureRoom,
        Pulled_Target_Temperature, UpdatedCurrentTemperature_Room,
        Heating_Offset_Value, TemperatureOffset, TemperatureOffsetRounded,
        Weekend;



        Triggering_item = event.itemName;

        root_room = Triggering_item.slice(0, (Triggering_item.indexOf('_') + 1) - 1);

        Heating_Active = String(root_room) + '_Heating_Active';

        Heating_AutomationMode = String(root_room) + '_Heating_AutomationMode';

        Heating_AutomationProfilePrimary = String(root_room) + '_Heating_AutomationProfilePrimary';

        Heating_AutomationProfileAlternate = String(root_room) + '_Heating_AutomationProfileAlternate';

        Heating_Offset = String(root_room) + '_Heating_Offset';

        Heating_WindowSuspension = String(root_room) + '_Heating_WindowSuspension';

        Heating_AutomationProfileActive = String(root_room) + '_Heating_AutomationProfileActive';

        Heating_BoostMode = String(root_room) + '_Heating_BoostMode';

        Heating_AwaySwitch = String(root_room) + '_Heating_AwaySwitch';

        Heating_AutomationProfileActiveID = String(root_room) + '_Heating_AutomationProfileActiveID';

        TemperatureCalibration = String(root_room) + '_thermostat_Temperature_Calibration';

        Kindersicherung = String(root_room) + '_thermostat_Kindersicherung';

        CurrentSetPoint = String(root_room) + '_thermostat_Current_Setpoint';

        SystemMode = String(root_room) + '_thermostat_System_Mode';

        Temperature = String(root_room) + '_Temperature';

        ThermostatTemperature = String(root_room) + '_thermostat_Local_Temperature';

        if (items.getItem(Heating_Active).state == 'NULL') {
          items.getItem(Heating_Active).postUpdate('OFF');
        }

        if (items.getItem(Heating_AutomationMode).state == 'NULL') {
          items.getItem(Heating_AutomationMode).postUpdate('Static');
        }

        if (items.getItem(Heating_AutomationProfilePrimary).state == 'NULL') {
          items.getItem(Heating_AutomationProfilePrimary).postUpdate('Abwesend');
          if (items.getItem(Heating_AutomationProfileAlternate).state == 'NULL') {
            items.getItem(Heating_AutomationProfileAlternate).postUpdate('Abwesend');
          }
        }

        if (items.getItem(Heating_Offset).state == 'NULL') {
          items.getItem(Heating_Offset).postUpdate(0);
        }

        if (items.getItem(Heating_WindowSuspension).state == 'NULL') {
          items.getItem(Heating_WindowSuspension).postUpdate('OFF');
        }

        if (items.getItem(Heating_BoostMode).state == 'NULL') {
          items.getItem(Heating_BoostMode).postUpdate('OFF');
        }

        AutomationMode = items.getItem(Heating_AutomationMode).state;

        if (AutomationMode != 'Auto' && AutomationMode != 'Static') {
          items.getItem(Heating_AutomationMode).postUpdate('Static');
        } else {

        }

        if (items.getItem('Global_Vacation_Override').state == 'ON') {
          items.getItem(Heating_AutomationProfileActive).postUpdate('Aus');
          ActiveProfile = items.getItem(Heating_AutomationProfileActive).state;
        } else {
          if (items.getItem(Heating_Active).state == 'ON') {
            if (items.getItem(Heating_WindowSuspension).state == 'ON') {
              items.getItem(Heating_AutomationProfileActive).postUpdate('Aus');
              ActiveProfile = items.getItem(Heating_AutomationProfileActive).state;
            } else {
              if (items.getItem(Heating_AwaySwitch).state == 'ON') {
                items.getItem(Heating_AutomationProfileActive).postUpdate('Abwesend');
                ActiveProfile = items.getItem(Heating_AutomationProfileActive).state;
              } else {
                if (items.getItem(Heating_BoostMode).state == 'ON') {
                  items.getItem(Heating_AutomationProfileActive).postUpdate('Boost');
                  ActiveProfile = items.getItem(Heating_AutomationProfileActive).state;
                } else {
                  if (items.getItem(Heating_AutomationMode).state == 'Static') {
                    items.getItem(Heating_AutomationProfileActive).postUpdate(items.getItem(Heating_AutomationProfilePrimary).state);
                    ActiveProfile = items.getItem(Heating_AutomationProfilePrimary).state;
                  } else {
                    if (items.getItem(Heating_AutomationMode).state == 'Auto') {
                      Weekend = ((time.ZonedDateTime.now()).dayOfWeek().value());
                      if (Weekend < 6) {
                        items.getItem(Heating_AutomationProfileActive).postUpdate(items.getItem(Heating_AutomationProfilePrimary).state);
                        ActiveProfile = items.getItem(Heating_AutomationProfilePrimary).state;
                      } else {
                        items.getItem(Heating_AutomationProfileActive).postUpdate(items.getItem(Heating_AutomationProfileAlternate).state);
                        ActiveProfile = items.getItem(Heating_AutomationProfileAlternate).state;
                      }
                    }
                  }
                }
              }
            }
          } else {
            items.getItem(Heating_AutomationProfileActive).postUpdate('Aus');
            ActiveProfile = items.getItem(Heating_AutomationProfileActive).state;
          }
        }

        var member_of_profile_list_list = items.getItem('ProfileList').members;

        for (var member_of_profile_list_index in member_of_profile_list_list) {
          member_of_profile_list = member_of_profile_list_list[member_of_profile_list_index];
          Profile_Group = [member_of_profile_list.name,'_','Name'].join('');
          Name_of_Profile = items.getItem(Profile_Group).state;
          if (Name_of_Profile == ActiveProfile) {
            items.getItem(Heating_AutomationProfileActiveID).sendCommand(member_of_profile_list.name);
            profile_calculated = [member_of_profile_list.name,'_','Calculated'].join('');
            Heating_Target = String(root_room) + '_Heating_Target';
            Pulled_Target_Temperature = items.getItem(profile_calculated).state;
            Heating_Offset = String(root_room) + '_Heating_Offset';
            Heating_Offset_Value = items.getItem(Heating_Offset).state;
            Target_Temperature = (Quantity(Pulled_Target_Temperature).add(Quantity(Heating_Offset_Value)));
            items.getItem(Heating_Target).sendCommand(Target_Temperature);
          }
        }

        if (items.getItem(Kindersicherung).state == 'UNLOCK') {
          items.getItem(Kindersicherung).sendCommand('LOCK');
        } else if (items.getItem(Kindersicherung).state == 'LOCK') {

        }

        if (ActiveProfile == 'Aus') {
          if (items.getItem(SystemMode).state == 'heat' || items.getItem(SystemMode).state == 'auto') {
            items.getItem(SystemMode).sendCommand('off');
          }
        } else if (ActiveProfile != 'Aus') {
          if (items.getItem(SystemMode).state != 'heat') {
            items.getItem(SystemMode).sendCommand('heat');
          }
        }

        if (items.getItem(CurrentSetPoint).state == Target_Temperature) {

        } else {
          items.getItem(CurrentSetPoint).sendCommand(Target_Temperature);
        }

        CurrentTemperatureRoom = Quantity(items.getItem(Temperature).state);

        UpdatedCurrentTemperature_Room = (Quantity(CurrentTemperatureRoom).add(Quantity('0.5')));

        if (UpdatedCurrentTemperature_Room < Target_Temperature) {
          TemperatureOffset = (Quantity(items.getItem(Temperature).state).subtract(Quantity(items.getItem(ThermostatTemperature).state)));
          TemperatureOffsetRounded = Math.round(TemperatureOffset);
          if (TemperatureOffset < -6) {
            TemperatureOffsetRounded = -6;
          } else if (TemperatureOffset > 6) {
            TemperatureOffsetRounded = 6;
          }
          items.getItem(TemperatureCalibration).sendCommand(TemperatureOffsetRounded);
        } else if (UpdatedCurrentTemperature_Room > CurrentSetPoint && items.getItem(TemperatureCalibration).state != '0') {
          items.getItem(TemperatureCalibration).sendCommand('0');
        }
    type: script.ScriptAction

Rule: Reset Boost Mode
configuration: {}
triggers:
  - id: "1"
    configuration:
      groupName: boostmode
      state: ON
    type: core.GroupStateUpdateTrigger
conditions: []
actions:
  - inputs: {}
    id: "2"
    configuration:
      type: application/javascript
      script: >
        var trigger_room, root_room, Boost_Item_Name;



        trigger_room = event.itemName;

        root_room = items.getItem(trigger_room).name.slice(0, (items.getItem(trigger_room).name.indexOf('_') + 1) - 1);

        Boost_Item_Name = String(root_room) + '_Heating_BoostMode';

        if (cache.private.exists('Boost_Off_Timer') === false || cache.private.get('Boost_Off_Timer').hasTerminated()) {
          cache.private.put('Boost_Off_Timer', actions.ScriptExecution.createTimer('Boost_Off_Timer', time.ZonedDateTime.now().plusMinutes(30), function () {
            items.getItem(Boost_Item_Name).postUpdate('OFF');
            cache.private.remove('Boost_Off_Timer');
          }));
        };
    type: script.ScriptAction

Rule: Reset Temperature Override
configuration: {}
triggers:
  - id: "1"
    configuration:
      groupName: override
    type: core.GroupStateChangeTrigger
conditions: []
actions:
  - inputs: {}
    id: "2"
    configuration:
      type: application/javascript
      script: >
        var trigger_room, root_room, Offset_Item_Name;



        trigger_room = event.itemName;

        root_room = items.getItem(trigger_room).name.slice(0, (items.getItem(trigger_room).name.indexOf('_') + 1) - 1);

        Offset_Item_Name = String(root_room) + '_Heating_Offset';

        if (cache.private.exists('Offset_Off_Timer') === false || cache.private.get('Offset_Off_Timer').hasTerminated()) {
          cache.private.put('Offset_Off_Timer', actions.ScriptExecution.createTimer('Offset_Off_Timer', time.ZonedDateTime.now().plusHours(3), function () {
            items.getItem(Offset_Item_Name).postUpdate('0');
            cache.private.remove('Offset_Off_Timer');
          }));
        };
    type: script.ScriptAction

Rule: Open Windows
configuration: {}
triggers:
  - id: "1"
    configuration:
      groupName: windowsensor
    type: core.GroupStateChangeTrigger
conditions: []
actions:
  - inputs: {}
    id: "2"
    configuration:
      type: application/javascript
      script: >
        var triggered_window_group, root_room, Heating_WindowSuspension;



        console.info('-----------------------------------');

        console.info('Window opened / closed - initiating update of Heating_WindowSuspension ');

        triggered_window_group = event.itemName;

        root_room = triggered_window_group.slice(0, (triggered_window_group.indexOf('_') + 1) - 1);

        Heating_WindowSuspension = String(root_room) + '_Heating_WindowSuspension';

        if (items.getItem(triggered_window_group).state != '0') {
          items.getItem(Heating_WindowSuspension).sendCommand('ON');
          console.info((['Window(s) open in room: ',root_room,'Set window Suspension switch to ON'].join('')));
        } else {
          items.getItem(Heating_WindowSuspension).sendCommand('OFF');
          console.info((['Window(s) closed in room: ',root_room,'Set Window Suspension Switch to OFF'].join('')));
        }
    type: script.ScriptAction

Rule Away Mode
configuration: {}
triggers:
  - id: "1"
    configuration:
      groupName: away
    type: core.GroupStateChangeTrigger
conditions: []
actions:
  - inputs: {}
    id: "2"
    configuration:

      type: application/javascript
      script: >
        var item_name_triggered, Count_Away, root_room, awaymode_switch,
        member_of_triggering_group;



        item_name_triggered = event.itemName;

        Count_Away = 0;

        root_room = items.getItem(item_name_triggered).name.slice(0, (items.getItem(item_name_triggered).name.indexOf('_') + 1) - 1);

        awaymode_switch = String(root_room) + '_Heating_AwaySwitch';

        var member_of_triggering_group_list = items.getItem(item_name_triggered).members;

        for (var member_of_triggering_group_index in member_of_triggering_group_list) {
          member_of_triggering_group = member_of_triggering_group_list[member_of_triggering_group_index];
          Count_Away = Count_Away + 1;
        }

        if (items.getItem(item_name_triggered).state == Count_Away) {
          items.getItem(awaymode_switch).sendCommand('ON');
        } else {
          items.getItem(awaymode_switch).sendCommand('OFF');
        }
    type: script.ScriptAction

Widget: Heating Settings Room
uid: Heizungseinstellungen_Raum_Popup
tags: []
props:
  parameters:
    - context: item
      description: The master item for the heating zone
      label: Master_Heating_Switch item
      name: root_item
      required: false
      type: TEXT
  parameterGroups: []
timestamp: Sep 20, 2023, 1:22:02 PM
component: f7-card
config:
  style:
    text-align: center
  text: "='Thermostat Control: ' + props.root_item.split('_')[0]"
slots:
  content:
    - component: f7-block
      config:
        style:
          margin-left: -2em
          margin-right: -2em
      slots:
        default:
          - component: f7-row
            config:
              style:
                margin-bottom: 20px
                margin-top: -25px
            slots:
              default:
                - component: f7-col
                  config:
                    width: 95
                  slots:
                    default:
                      - component: oh-icon
                        config:
                          align: right
                          icon: oh:away.svg
                          visible: =(items[props.root_item.split('_')[0] + '_Heating_AwaySwitch'].state == 'ON')? true:false
                          width: 32px
                - component: f7-col
                  config:
                    width: 5
                  slots:
                    default:
                      - component: oh-icon
                        config:
                          align: right
                          icon: oh:window-false.svg
                          visible: =(items[props.root_item.split('_')[0] + '_Heating_WindowSuspension'].state == 'ON')? true:false
                          width: 32px
          - component: f7-row
            config:
              style:
                margin-bottom: 20px
                margin-top: 10px
                text-align: center
                width: 100%
            slots:
              default:
                - component: f7-col
                  config:
                    style:
                      margin-left: 20px
                      text-align: right
                  slots:
                    default:
                      - component: Label
                        config:
                          text: Heizungszone aktiv
                - component: f7-col
                  config:
                    style:
                      text-align: left
                  slots:
                    default:
                      - component: oh-toggle
                        config:
                          item: =props.root_item
                - component: f7-col
                  config:
                    style:
                      text-align: right
                  slots:
                    default:
                      - component: Label
                        config:
                          text: "Boost:"
                - component: f7-col
                  config:
                    style:
                      text-align: left
                  slots:
                    default:
                      - component: oh-toggle
                        config:
                          item: =props.root_item.split('_')[0] + '_Heating_BoostMode'
          - component: f7-row
            config:
              style:
                margin-top: 10px
            slots:
              default:
                - component: f7-col
                  config:
                    style:
                      border-right: 2px solid white
                      text-align: center
                      width: 50%
                  slots:
                    default:
                      - component: f7-row
                        slots:
                          default:
                            - component: Label
                              config:
                                style:
                                  width: 100%
                                text: Aktuelle Temperatur
                      - component: f7-row
                        config:
                          style:
                            text-align: center
                        slots:
                          default:
                            - component: Label
                              config:
                                style:
                                  font-size: 18pt
                                  font-weight: bold
                                  width: 100%
                                text: =items[props.root_item.split('_')[0] + '_Temperature'].state + ' °C'
                - component: f7-col
                  config:
                    style:
                      width: 50%
                  slots:
                    default:
                      - component: f7-row
                        slots:
                          default:
                            - component: Label
                              config:
                                style:
                                  width: 100%
                                text: Zieltemperatur
                      - component: f7-row
                        slots:
                          default:
                            - component: Label
                              config:
                                style:
                                  font-size: 18pt
                                  font-weight: bold
                                  width: 100%
                                text: =items[props.root_item.split('_')[0] + '_Heating_Target'].state + ' °C'
          - component: f7-row
            config:
              style:
                margin-top: 20px
                text-align: center
                width: 100%
            slots:
              default:
                - component: Label
                  config:
                    style:
                      margin-bottom: -15px
                      width: 100%
                    text: ◄◄ Override ►►
                - component: f7-col
                  config:
                    style:
                      margin: 10px
                      padding: 10px
                  slots:
                    default:
                      - component: oh-slider
                        config:
                          item: =props.root_item.split('_')[0] + '_Heating_Offset'
                          label: true
                          max: 3
                          min: -3
                          releaseOnly: true
                          scale: true
                          scaleSteps: 6
                          step: 0.5
                          style:
                            --f7-range-bar-active-bg-color: "#919191"
                            --f7-range-bar-bg-color: "#919191"
                          unit: °C
          - component: f7-row
            config:
              style:
                margin-top: 10px
                text-align: center
                width: 100%
            slots:
              default:
                - component: f7-segmented
                  config:
                    round: true
                    style:
                      padding: 10px
                      width: 100%
                  slots:
                    default:
                      - component: oh-button
                        config:
                          action: command
                          actionCommand: Auto
                          actionItem: =props.root_item.split('_')[0] + '_Heating_AutomationMode'
                          active: "=(items[props.root_item.split('_')[0] + '_Heating_AutomationMode'].state == 'Auto') ? true : false"
                          outline: true
                          round: true
                          text: Automatisch
                      - component: oh-button
                        config:
                          action: command
                          actionCommand: Static
                          actionItem: =props.root_item.split('_')[0] + '_Heating_AutomationMode'
                          active: "=(items[props.root_item.split('_')[0] + '_Heating_AutomationMode'].state == 'Static') ? true : false"
                          outline: true
                          round: true
                          text: Statisch
          - component: f7-row
            config:
              style:
                margin-top: 20px
                text-align: center
                width: 100%
            slots:
              default:
                - component: f7-col
                  slots:
                    default:
                      - component: Label
                        config:
                          style:
                            text-align: right
                          text: Aktives Profil
                - component: f7-col
                  slots:
                    default:
                      - component: f7-row
                        slots:
                          default:
                            - component: Label
                              config:
                                style:
                                  background-color: "=( items[props.root_item.split('_')[0] + '_Heating_AutomationProfileActive'].state == 'Abwesend' ? '#919191' : ( items[props.root_item.split('_')[0] + '_Heating_AutomationProfileActive'].state == 'Aus' ? '#00ACB5' : ( items[props.root_item.split('_')[0] + '_Heating_AutomationProfileActive'].state == 'Boost' ? '#008c09' : '#902022')))"
                                  border-radius: 10px
                                  font-weight: bold
                                  padding: 0px 10px 0px 10px
                                text: =items[props.root_item.split('_')[0] + '_Heating_AutomationProfileActive'].state
          - component: f7-row
            config:
              style:
                margin-bottom: 10px
                margin-top: 10px
            slots:
              default:
                - component: f7-col
                  config:
                    style:
                      text-align: center
                      width: 50%
                  slots:
                    default:
                      - component: f7-row
                        slots:
                          default:
                            - component: Label
                              config:
                                style:
                                  width: 100%
                                text: Montag - Freitag
                      - component: f7-row
                        config:
                          style:
                            justify-content: center
                            margin-top: 10px
                            padding: 0px 20px 0px 20px
                            text-align: center
                        slots:
                          default:
                            - component: oh-button
                              config:
                                action: popup
                                actionModal: widget:Auswahl_Heizungsprofile
                                actionModalConfig:
                                  item: =props.root_item.split('_')[0] + '_Heating_AutomationProfilePrimary'
                                fill: true
                                round: true
                                style:
                                  background-color: "=( items[props.root_item.split('_')[0] + '_Heating_AutomationProfilePrimary'].state == 'Away' ? '#919191' : ( items[props.root_item.split('_')[0] + '_Heating_AutomationProfilePrimary'].state == 'Frostguard' ? '#00ACB5' : '#902022'))"
                                  font-weight: bold
                                text: =items[props.root_item.split('_')[0] + '_Heating_AutomationProfilePrimary'].state
                - component: f7-col
                  config:
                    style:
                      width: 50%
                  slots:
                    default:
                      - component: f7-row
                        slots:
                          default:
                            - component: Label
                              config:
                                style:
                                  width: 100%
                                text: Samstag - Sonntag
                      - component: f7-row
                        config:
                          style:
                            justify-content: center
                            margin-top: 10px
                            padding: 0px 20px 0px 20px
                            text-align: center
                        slots:
                          default:
                            - component: oh-button
                              config:
                                action: popup
                                actionModal: widget:Auswahl_Heizungsprofile
                                actionModalConfig:
                                  item: =props.root_item.split('_')[0] + '_Heating_AutomationProfileAlternate'
                                fill: true
                                round: true
                                style:
                                  background-color: "=( items[props.root_item.split('_')[0] + '_Heating_AutomationProfileAlternate'].state == 'Abwesend' ? '#919191' : ( items[props.root_item.split('_')[0] + '_Heating_AutomationProfileAlternate'].state == 'Aus' ? '#00ACB5' : '#902022'))"
                                  font-weight: bold
                                text: =items[props.root_item.split('_')[0] + '_Heating_AutomationProfileAlternate'].state
          - component: f7-row
            config:
              style:
                margin-bottom: 10px
                margin-top: 10px
            slots:
              default:
                - component: f7-col
                  slots:
                    default:
                      - component: widget:Profilgraph
                        config:
                          profilecore: =items[props.root_item.split('_')[0] + '_Heating_AutomationProfileActiveID'].state

Widget: Modify Temperature per Room
uid: Heizungsprofil_Einstellungen
tags: []
props:
  parameters:
    - context: item
      description: Profile
      label: Profil-Gruppe
      name: profile
      required: true
      type: TEXT
  parameterGroups: []
timestamp: Sep 19, 2023, 8:37:38 PM
component: f7-card
config:
  style:
    --f7-range-bar-active-bg-color: "#ffb17d"
    --f7-range-bar-bg-color: "#fae2d2"
    --f7-range-bar-border-radius: 1em
    --f7-range-bar-size: 0.6em
    position: relative
  title: ="Feineinstellung Heizungsprofil für " + items[props.profile + "_Name"].state
  width: 100%
slots:
  default:
    - component: f7-row
      slots:
        default:
          - component: f7-col
            config: {}
            slots:
              default:
                - component: Label
                  config:
                    style:
                      font-size: 12pt
                      padding: 10px
          - component: f7-col
            config:
              style:
                width: 5%
            slots:
              default:
                - component: oh-button
                  config:
                    action: popup
                    actionModal: widget:Heizeinstellungen_Profil_Popup
                    actionModalConfig:
                      profile: =props.profile
                    color: black
                    iconF7: gear_alt_fill
                    large: true
    - component: f7-block
      config:
        style:
          display: flex
          flex-direction: row
          height: 230px
          left: 2.5%
          top: 0%
          width: 100%
      slots:
        default:
          - component: oh-repeater
            config:
              for: item
              fragment: true
              groupItem: =props.profile + '_val'
              sourceType: itemsInGroup
            slots:
              default:
                - component: f7-col
                  config:
                    style:
                      height: 210px
                      width: 100%
                  slots:
                    default:
                      - component: f7-row
                        slots:
                          default:
                            - component: oh-slider
                              config:
                                item: =loop.item.name
                                label: true
                                max: 30
                                min: 5
                                releaseOnly: true
                                scale: "=(loop.item.label == '00') ? true : false"
                                scaleSteps: 5
                                step: 0.5
                                style:
                                  --f7-range-bar-active-bg-color: "=(dayjs().format('HH') == loop.item.label) ? '#298a29' : '' "
                                  height: 150px
                                title: =loop.item.label
                                vertical: 1
                      - component: f7-row
                        slots:
                          default:
                            - component: Label
                              config:
                                style:
                                  position: relative
                                  top: 35px
                                  transform: rotate(-70deg)
                                  width: 1px
                                text: =loop.item.label + ":00"
                                visible: true
    - component: Label
      config:
        style:
          height: 10px
          left: 0px
          position: absolute
          top: 205px
          transform: rotate(-90deg)
          width: 1px
        text: Temperatur

2 Likes

@wAvE @Marmoset_Threat
Thanks for sharing and creating!

Thanks again to @Marmoset_Threat and @wAvE for the great work.

I am under openHAB 4.0.4

Here are a few more comments from me on the post by @wAvE.

It is important to note that the items for the heater thermostat must be adjusted in the rule. I have put my rule Heating Control (Rule: Regular Heating Check and Rule: Heating Input Trigger combined) below again. In the rule states in the comments which items of the thermostat and must be adjusted. I have also removed the Kindersicherung as I don’t have it and I still have to look at the calibration, but I don’t think it works with my heater thermostat.

Enclosed is also the page (Page Heating Control) in my openHAB as an example, because at the beginning it was not clear to me where and how to call the widgets. Here you have to adapt the settings to your items. My shortened items file is also attached.

Rule Heating Control
configuration: {}
triggers:
  - id: "1"
    configuration:
      cronExpression: 0 0/10 * * * ? *
    type: timer.GenericCronTrigger
  - id: "3"
    configuration:
      command: ON
      itemName: Trigger_HeatAutomation_Script
    type: core.ItemCommandTrigger
  - id: "4"
    configuration:
      itemName: Global_Vacation_Override
    type: core.ItemStateChangeTrigger
  - id: "5"
    configuration:
      groupName: Heating_Input
    type: core.GroupStateChangeTrigger
conditions: []
actions:
  - inputs: {}
    id: "2"
    label: JavaScript
    description: JavaScript
    configuration:
      type: application/javascript
      script: >
        /*
         *  Version   Description                                      Date
         * ---------|------------------------------------------------|---------
         *    1.0     Original
         *    1.1     Added Quantity types                             22.08.24
         *
         * Note:
         *  The current temperature of the room must be added manually to each _temperature group.
         */

        var activeProfile;

        var automationMode;

        var heatingOffsetValue;

        var itemCurrentSetPoint;

        var itemHeatingActive;

        var itemHeatingAutoMode;

        var itemHeatingAutoProfileActive;

        var itemHeatingAutoProfileActiveID;

        var itemHeatingAutoProfileAlternate;

        var itemHeatingAutoProfilePrimary;

        var itemHeatingAwaySwitch;

        var itemHeatingBoostMode;

        var itemHeatingOffset;

        var itemHeatingWindowSuspension;

        var itemSystemMode;

        var itemTemperature;

        var itemTemperatureAtThermostat;

        var itemTemperatureCalibration;

        var itemTemperatureTarget;

        var memberOfMasterHeatingSwitchGroup; 

        var memberOfProfileList;

        var log                               =
        items.Heating_AutomationLogging.state;

        var loopSingleRoom; 

        var loopTroughAllRooms; 

        var nameTriggeringItem;

        var profileCalculated;

        var profileGroup;

        var profileName;

        var rootRoom;

        var temperatureOffset;

        var temperatureOffsetRounded;

        var temperaturePulledToTarget;

        var temperatureRoomCurrent;

        var temperatureRoomCurrentUpdated;

        var temperatureSetPointCurrent;

        var temperatureSetPointUpdated;

        var temperatureTarget;

        var thread                            = Java.type('java.lang.Thread');

        var weekend;




        if (log == 'ON') console.info ('this.event.type: ' + this.event.type )


        switch(this.event.type) {
          case 'TimerEvent':          // Cron
          case 'ItemCommandEvent':    // Trigger_HeatAutomation_Script
            loopTroughAllRooms = true;
            break;
          case 'ItemStateChangedEvent':
            if (event.itemName == 'Global_Vacation_Override') {
              loopTroughAllRooms = true;
            } else {
              loopSingleRoom = true;
            }
            break;
          default:
            loopTroughAllRooms = true;
            break;
        } 
          

        if (loopTroughAllRooms) {
          items.getItem('Trigger_HeatAutomation_Script').sendCommand('OFF');

          thread.sleep(2000);

          var memberOfMasterHeatingSwitchGroupList = items.getItem('Master_Heating_Switch').members;
          for (var memberOfMasterHeatingSwitchGroupIndex in memberOfMasterHeatingSwitchGroupList) {
            memberOfMasterHeatingSwitchGroup = memberOfMasterHeatingSwitchGroupList[memberOfMasterHeatingSwitchGroupIndex];
            rootRoom                         = memberOfMasterHeatingSwitchGroup.name.slice(0, (memberOfMasterHeatingSwitchGroup.name.indexOf('_') + 1) - 1).toString();
            mainHeatingControl();
          }
        } else if (loopSingleRoom) {
          nameTriggeringItem               = event.itemName;
          rootRoom                         = nameTriggeringItem.slice(0, (nameTriggeringItem.indexOf('_') + 1) - 1).toString();
          mainHeatingControl();  
        }

          
        /*------------------------------------------------*/

        /*  Functions                                     */

        /*------------------------------------------------*/

        function mainHeatingControl() {  
          itemCurrentSetPoint              = rootRoom + 'Heizkorperthermostat_Setpointheat';
          itemHeatingActive                = rootRoom + '_Heating_Active';
          itemHeatingAutoMode              = rootRoom + '_Heating_AutomationMode';
          itemHeatingAutoProfileActive     = rootRoom + '_Heating_AutomationProfileActive';
          itemHeatingAutoProfileActiveID   = rootRoom + '_Heating_AutomationProfileActiveID';
          itemHeatingAutoProfileAlternate  = rootRoom + '_Heating_AutomationProfileAlternate';
          itemHeatingAutoProfilePrimary    = rootRoom + '_Heating_AutomationProfilePrimary';
          itemHeatingAwaySwitch            = rootRoom + '_Heating_AwaySwitch';
          itemHeatingBoostMode             = rootRoom + '_Heating_BoostMode';
          itemHeatingOffset                = rootRoom + '_Heating_Offset';
          itemHeatingWindowSuspension      = rootRoom + '_Heating_WindowSuspension';
          itemSystemMode                   = rootRoom + 'Heizkorperthermostat_Thermostatmode';         // adjust to set your Heater thermostat
          itemTemperature                  = rootRoom + '_Temperature';                                // add to this group your temperature room items
          itemTemperatureAtThermostat      = rootRoom + 'Heizkorperthermostat_Currenttemperature';     // adjust to set your Heater thermostat
        //itemTemperatureCalibration       = rootRoom +
        'Heizkorperthermostat_Temperature_Calibration';// adjust to set your
        Heater thermostat

          if (items.getItem(itemHeatingActive).state == 'NULL') {
        	items.getItem(itemHeatingActive).postUpdate('OFF');
          }
          if (items.getItem(itemHeatingAutoMode).state == 'NULL') {
        	items.getItem(itemHeatingAutoMode).postUpdate('Static');
          }
          if (items.getItem(itemHeatingAutoProfilePrimary).state == 'NULL') {
        	items.getItem(itemHeatingAutoProfilePrimary).postUpdate('Abwesend');
        	if (items.getItem(itemHeatingAutoProfileAlternate).state == 'NULL') {
        	  items.getItem(itemHeatingAutoProfileAlternate).postUpdate('Abwesend');
        	}
          }
          if (items.getItem(itemHeatingOffset).state == 'NULL') {
        	items.getItem(itemHeatingOffset).postUpdate(0);
          }
          if (items.getItem(itemHeatingWindowSuspension).state == 'NULL') {
        	items.getItem(itemHeatingWindowSuspension).postUpdate('OFF');
          }
          if (items.getItem(itemHeatingBoostMode).state == 'NULL') {
        	items.getItem(itemHeatingBoostMode).postUpdate('OFF');
          }
          automationMode = items.getItem(itemHeatingAutoMode).state;
          if (automationMode != 'Auto' && automationMode != 'Static') {
        	items.getItem(itemHeatingAutoMode).postUpdate('Static');
          } else {
          }
          if (items.getItem('Global_Vacation_Override').state == 'ON') {
        	items.getItem(itemHeatingAutoProfileActive).postUpdate('Away');
        	activeProfile = items.getItem(itemHeatingAutoProfileActive).state;
          } else {
        	if (items.getItem(itemHeatingActive).state == 'ON') {
        	  if (items.getItem(itemHeatingWindowSuspension).state == 'ON') {
        		items.getItem(itemHeatingAutoProfileActive).postUpdate('Aus');
        		activeProfile = items.getItem(itemHeatingAutoProfileActive).state;
        	  } else {
        		if (items.getItem(itemHeatingAwaySwitch).state == 'ON') {
        		  items.getItem(itemHeatingAutoProfileActive).postUpdate('Away');
        		  activeProfile = items.getItem(itemHeatingAutoProfileActive).state;
        		} else {
        		  if (items.getItem(itemHeatingBoostMode).state == 'ON') {
        			items.getItem(itemHeatingAutoProfileActive).postUpdate('Boost');
        			activeProfile = items.getItem(itemHeatingAutoProfileActive).state;
        		  } else {
        			if (items.getItem(itemHeatingAutoMode).state == 'Static') {
        			  items.getItem(itemHeatingAutoProfileActive).postUpdate(items.getItem(itemHeatingAutoProfilePrimary).state);
        			  activeProfile = items.getItem(itemHeatingAutoProfilePrimary).state;
        			} else {
        			  if (items.getItem(itemHeatingAutoMode).state == 'Auto') {
        				weekend = ((time.ZonedDateTime.now()).dayOfWeek().value());
        				if (weekend < 6) {
        				  items.getItem(itemHeatingAutoProfileActive).postUpdate(items.getItem(itemHeatingAutoProfilePrimary).state);
        				  activeProfile = items.getItem(itemHeatingAutoProfilePrimary).state;
        				} else {
        				  items.getItem(itemHeatingAutoProfileActive).postUpdate(items.getItem(itemHeatingAutoProfileAlternate).state);
        				  activeProfile = items.getItem(itemHeatingAutoProfileAlternate).state;
        				}
        			  }
        			}
        		  }
        		}
        	  }
        	} else {
        	  items.getItem(itemHeatingAutoProfileActive).postUpdate('Aus');
        	  activeProfile = items.getItem(itemHeatingAutoProfileActive).state;
        	}
          }
          var memberOfProfileListList = items.getItem('ProfileList').members;
          for (var memberOfProfileListIndex in memberOfProfileListList) {
            memberOfProfileList = memberOfProfileListList[memberOfProfileListIndex];
        	profileGroup        = [memberOfProfileList.name,'_','Name'].join('');
        	profileName         = items.getItem(profileGroup).state;
            if (profileName == activeProfile) {
              items.getItem(itemHeatingAutoProfileActiveID).sendCommand(memberOfProfileList.name);
        	  profileCalculated         = [memberOfProfileList.name,'_','Calculated'].join('');
        	  itemTemperatureTarget     = rootRoom + '_Heating_Target';
        	  temperaturePulledToTarget = items.getItem(profileCalculated).state;
        	  itemHeatingOffset         = rootRoom + '_Heating_Offset';
        	  temperatureOffsetValue    = items.getItem(itemHeatingOffset).state;
        	  temperatureTarget         = temperaturePulledToTarget + temperatureOffsetValue;
              items.getItem(itemTemperatureTarget).sendCommand(temperatureTarget);
        	}
          }

          if (log == 'ON') console.info('------------------------------------------------------------------------');
          if (log == 'ON') console.info('Room: ' + rootRoom);
          if (log == 'ON') console.info('------------------------------------------------------------------------');

          if (activeProfile == 'Aus') {
            // OFF = 0, HEAT = 1, HEAT_ECON = 2, FULL_POWER = 3
            if (items.getItem(itemSystemMode).state == '1' || items.getItem(itemSystemMode).state == '2' || items.getItem(itemSystemMode).state == '3') {
        	  items.getItem(itemSystemMode).sendCommand('0');
        	}
            if (log == 'ON') console.info('No active profile');
          } else if (activeProfile != 'Aus') {
        	if (items.getItem(itemSystemMode).state != '1') {
        	  items.getItem(itemSystemMode).sendCommand('1');
        	}
            if (log == 'ON') console.info('Check if Current SetPoint of Thermostat matches the Target temperature and change it if needed');
            temperatureSetPointCurrent = (Quantity(items.getItem(itemCurrentSetPoint).state));
            temperatureSetPointUpdated = (Quantity(items.getItem(itemTemperatureTarget).state));
            if (log == 'ON') console.info('Current Setpoint is: ' + (Quantity(temperatureSetPointCurrent)));
            if (log == 'ON') console.info('Current Target Temp is: ' + (Quantity(temperatureSetPointUpdated)));
            if (Quantity(temperatureSetPointCurrent).equal(Quantity(temperatureSetPointUpdated))) {  
              if (log == 'ON') console.info('Thermostat temperature already set to target temperature - do nothing');
            } else {
              if (log == 'ON') console.info('Thermostat temperature not matching target temperature - Change temperature: ' + (Quantity(temperatureSetPointUpdated)));
              items.getItem(itemCurrentSetPoint).sendCommand(temperatureSetPointUpdated);
            }
            if (log == 'ON') console.info('Start Thermostat Calibration Script for ' + rootRoom);
            if (log == 'ON') console.info('Adding 0.5 °C to CurrentTemperature for less sensivity');
            temperatureRoomCurrent        = (Quantity(items.getItem(itemTemperature).state));
            temperatureRoomCurrentUpdated = (Quantity(temperatureRoomCurrent).add(Quantity('0.5' + '°C')));
            
            if (Quantity(temperatureRoomCurrentUpdated).lessThan(Quantity(temperatureSetPointUpdated))) {
              if (log == 'ON') console.info('Room Temperature Lower than temperatureTarget modify Temperature Offset to adjust it');
              if (log == 'ON') console.info('Calculate needed Offset to match Sensor Temperature and Thermostat Temperature');
              if (log == 'ON') console.info('Check if Thermostat Temp is under Room Temp');
              if (log == 'ON') console.info('Room Temperature is: '       + (Quantity(items.getItem(itemTemperature).state)));
              if (log == 'ON') console.info('Thermostat Temperature is: ' + (Quantity(items.getItem(itemTemperatureAtThermostat).state)));
              temperatureOffset = (Quantity(items.getItem(itemTemperature).state).subtract(Quantity(items.getItem(itemTemperatureAtThermostat).state)));
              temperatureOffsetRounded = Math.round(temperatureOffset);
              if (temperatureOffset < -6) {
                temperatureOffsetRounded = -6;
              } else if (temperatureOffset > 6) {
                temperatureOffsetRounded = 6;
              }
              //items.getItem(itemTemperatureCalibration).sendCommand(temperatureOffsetRounded);
              if (log == 'ON') console.info('Offset Calculated and updated to: ' + (Quantity(temperatureOffsetRounded)));
              if (log == 'ON') console.info('Room Temperature Higher than Target temperature, reset Temperature Calibration');
              //console.info('Current Temperature Calibration is: ' + items.getItem(itemTemperatureCalibration).state);
             /*} else if (temperatureRoomCurrentUpdated > Number.parseFloat(items.getItem(itemCurrentSetPoint).state) && items.getItem(itemTemperatureCalibration).state != '0') {
              console.info('Room Temperature Higher than Target temperature, reset Temperature Calibration');
              items.getItem(itemTemperatureCalibration).sendCommand('0');*/
            } else {
              if (log == 'ON') console.info('Temperature already properly calibrated - do nothing');
            }
          }
        }
    type: script.ScriptAction
Items Heating Control
//Heating Control Items (Virtual)
//Global Groups
Switch Global_Vacation_Override "Heizung aus / Sommermodus / Away Modus"
Group windowsensor
Group:Number:COUNT(ON) boostmode
Switch Trigger_HeatAutomation_Script
Switch Heating_AutomationLogging "Heating Automation Logging On/Off" 
Group away
Group override


//Heat Specific Groups
Group Heating_Input
Group Master_Heating_Switch 


//Heating items for Schlafzimmer
Group Heating_Schlafzimmer (Schlafzimmer)

Switch Schlafzimmer_Heating_Active "Heating Active" (Master_Heating_Switch, Heating_Schlafzimmer, Heating_Input)  
Number:Temperature Schlafzimmer_Heating_Offset "Heating Offset" (Heating_Schlafzimmer, Heating_Input, override)  
Number:Temperature Schlafzimmer_Heating_Target "Heating Target" (Heating_Schlafzimmer, Heating_Input)  
String Schlafzimmer_Heating_AutomationMode "Heating Automation Mode" (Heating_Schlafzimmer, Heating_Input)  
String Schlafzimmer_Heating_AutomationProfilePrimary "Heating Automation Primary Profile" (Heating_Schlafzimmer, Heating_Input)  
String Schlafzimmer_Heating_AutomationProfileAlternate "Heating Automation Alternate Profile" (Heating_Schlafzimmer, Heating_Input)  
String Schlafzimmer_Heating_AutomationProfileActive "Heating Automation Active Profile" (Heating_Schlafzimmer, Heating_Input)  
String Schlafzimmer_Heating_AutomationProfileActiveID "Heating Automation Active Profile ID" (Heating_Schlafzimmer)  
Switch Schlafzimmer_Heating_BoostMode "Heating Boost Mode" (Heating_Schlafzimmer, boostmode, Heating_Input)  
Switch Schlafzimmer_Heating_WindowSuspension "Heating Window Suspension" (Heating_Schlafzimmer, Heating_Input)
Group:Number:Temperature:AVG Schlafzimmer_Temperature "Average Temperature" <temperature> (Heating_Schlafzimmer, Heating_Input)  
Group:Number:COUNT(FALSE) Schlafzimmer_Windowsensor "Anzahl offene Fenster" (Heating_Schlafzimmer, windowsensor, Heating_Input) 
Group:Number:COUNT(OFF) Schlafzimmer_Away "Person1 / Person2 Away" (Heating_Schlafzimmer, away, Heating_Input)
Switch Schlafzimmer_Heating_AwaySwitch "Schlafzimmer Away Switch" (Heating_Schlafzimmer, Heating_Input)
Page Heating Control
config:
  label: Heizungsteuerung
  sidebar: true
  order: "40"
blocks:
  - component: oh-block
    config: {}
    slots:
      default:
        - component: oh-grid-row
          config: {}
          slots:
            default:
              - component: oh-grid-col
                config: {}
                slots:
                  default:
                    - component: oh-list-card
                      config:
                        accordionList: false
                        mediaList: true
                      slots:
                        default:
                          - component: oh-list-item
                            config:
                              title: Temperaturen
                          - component: oh-label-item
                            config:
                              action: popup
                              actionModal: widget:Heizungseinstellungen_Raum_Popup
                              actionModalConfig:
                                root_item: Schlafzimmer_Heating_Active
                              icon: temperature
                              title: Schlafzimmer
                              item: SchlafzimmerHeizkorperthermostat_Currenttemperature
        - component: oh-grid-row
          config: {}
          slots:
            default:
              - component: oh-grid-col
                config: {}
                slots:
                  default:
                    - component: oh-list-card
                      config: {}
                      slots:
                        default:
                          - component: oh-list-item
                            config:
                              title: Einstellungen
                          - component: oh-label-item
                            config:
                              icon: oh:switch
                              iconUseState: true
                              item: Schlafzimmer_Heating_Active
                              title: Heizung Schalfzimmer
                          - component: oh-label-item
                            config:
                              action: popup
                              actionModal: widget:Liste_Heizungsprofile
                              icon: temperature
                              title: Einstellungen Heizungsprofile
        - component: oh-grid-row
          config: {}
          slots:
            default:
              - component: oh-grid-col
                config: {}
                slots:
                  default:
                    - component: oh-list-card
                      config: {}
                      slots:
                        default:
                          - component: oh-list-item
                            config:
                              title: Aktivieren des Logging
                          - component: oh-toggle-item
                            config:
                              icon: oh:switch
                              item: Heating_AutomationLogging
                              title: Logging
        - component: oh-grid-row
          config: {}
          slots:
            default: []
masonry: null
grid: null
canvas: null
Rule Heating Control Open Windows
configuration: {}
triggers:
  - id: "2"
    configuration:
      groupName: windowsensor
    type: core.GroupStateChangeTrigger
conditions: []
actions:
  - inputs: {}
    id: "3"
    label: Blockly
    description: Blockly
    configuration:

2 Likes

Hi @HolBaum5 is this your full setup working with in OH4 or just the changes you did to the full logic from @wAvE?

Also the items you still create it *.items file, correct?

Hello @pking,

I am currently on 4.1.2 and everything is running fine.

You need everything that @wAvE has described (I have only summarized the two rules), including the profiles, items and widgets.

For the profile items I have created a *.item file for the sake of simplicity.

Hi @HolBaum5 ,thanks for the feedback.
Understood. But would you say it is easier to install with your combined setup plus the remaining from @wAvE or ratherstart off with the initall setup from @wAvE ?

Follow the instructions from wAvE and follow my instructions then it should work.

Thanks @HolBaum5 . I managed to install but ending up similarily with the name errors. See below:

I just cant find in the settings what is wrong. Any ideas.