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
2 Likes