Post

Automating the HVAC System

Automating the HVAC System

In the last post, we created a bunch of sensors and automations to make it possible to control the HVAC system. This time, we’re going to build the portions of the system which control the thermostat, raising and lowering the temperature as desired.

Before we get started though, let’s determine how we actually want this to function. In plain English, what we want to do is:

Periodicallly check the current actual temperature in all the rooms, compare that against the desired temperature for all rooms, determine whether the actual temperature is within the range we want, and if not, update the thermostat’s set temperature in order to reach the desired temperature.

With that in mind, as we do whenever faced wwith complex problem we’ll break this up into a number of smaller problems to solve. The first separation we’ll do (and you’ll see why later) is to separate “what to do” from “when to do it” and focus first on what to do:

  • Get all current temps
  • Get all desired temps
  • Determine whether current temp is correct
  • If not, update thermostat

So let’s get into it!

But first, a huge caveat.

The solution we’re building here does not attempt to make each room be a different temperature; that requires expensive hardware and another integration we’ll deal with in a later post. Since most HVAC installations won’t be able to target different temperatures for different rooms, I’m presenting the most common one: whole house or one of a dual zone (e.g. upstairs and downstairs).

Building a script

Why and initial steps

For the “what to do” portion, we’re going to create a script, which is a set of conditions and actions which are executed when the script is called. The reason we’re going to use a script instead of an automation is because then it can be used by many different automations if we choose to do so. We also get some help through the use of variable the script expects to have; this will ensure future changes are easier to make as we know what information/sensors/inputs we’ll need.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
script:
  update_thermostat_temp:
    alias: Climate - Update Thermostat Temperature
    mode: restart # this means "if this is running and something else triggers it again, stop and restart the script"

    fields:
      # this is where we'll put the information we want to provide to the script

    variables:
      # this sets some internal variables for use later in the actions

    sequence:
      # the sequence of actions we will take

As before, we’ll introduce the individual chunks of code as we talk about each piece, then bring it all together at the end.

Get all current temperatures

As mentioned in the last post, we set up a temperature sensor in each room we care about. When we’re determining the current temperature, we’ll want to do a simple min/max operation. For example, if we’re in cooling mode (AC), the “current temperature” is the hottest of all the rooms because we want to get all rooms to the target temperature, and unless we cool the hottest room to that target, we will not have succeeded. The reverse is true if we’re in heating mode; we want to select the coolest of the rooms.

To make this happen, we’ll need to make the script aware of the room temperature sensors as well as what mode we’re in. Let’s first create a couple fields for this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
fields:
  thermostat_temp_sensor:
    name: Thermostat Temperature Sensor
    description: >-
      The temperature the thermostat currently thinks it is. This can often
      be different than the temperature in multiple rooms, yet controls the
      real-world behavior of the system, so we need to include it
    example: "sensor.my_thermostat_temperature"
    required: true
    selector:
      entity:
        filter:
          - device_class: temperature
            domain: sensor

  room_temp_sensors:
    name: Room Temperature Sensors
    description: >-
      A collection of room temperatures which should be considered when
      calculating whether to engage or disengage the HVAC system. These are
      likely to be temp sensors in the rooms you want covered.
    example: "['sensor.office_temperature', 'sensor.bedroom_temperature']"
    required: true
    selector:
      entity:
        filter:
          - device_class: temperature
            domain: sensor
        multiple: true

  current_hvac_mode:
    name: Current HVAC Mode
    description: >-
      The mode which the HVAC system is currently in. Accepted values are
      "cool" and "heat".
    example: cool
    required: true
    selector:
      select:
        options:
          - cool
          - heat

Since these are in the fields dictionary, we’ll be expected to pass these into the script, which itself is going to expect the values later on when defining variables or when performing actions. These are full examples of the fields, including the selector details which are useful when calling the script from the UI, whether directly or in an automation. We’ll also want the sensor on the thermostat itself so we can make sure that we’re getting a full view of the temperature spread.

When the script is executing, we’ll need to do the min/max operation as mentioned before; in our case, we’ll do that in the variables block so we can set it once and refer to it multiple times:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
variables:
      # add the thermostat temp sensor to the list of room temp sensors
      all_temp_sensors: >
        {{ room_temp_sensors + [thermostat_temp_sensor] }}

      # Gather a list of the temperatures for all sensors
      all_current_temps: >
        {{ all_temp_sensors | map('states') | select('match', '^[0-9.]+$') |  map('int') | list }}

      # Determine the temp to use as the current temp; max temp if cooling, min temp
      # for heating. More description: if we're heating, we want to make the
      # coldest room the current temp so that can be heated to the desired temp.
      # For cooling, we want to cool the hottest room to the target temp
      current_aggregate_temp: >-
        {% if current_hvac_mode == "cool" %}
          {{ all_current_temps | max }}
        {% else %}
          {{ all_current_temps | min }}
        {% endif %}

As you can see, these variables are making use of the fields defined above (room_temp_sensors, current_hvac_mode, and thermostat_temp_sensor).

At this point, we have the current temperature (current_aggregate_temp) which we’ll use to compare against the target temps and ultimately set the thermostat

Get all desired temperatures

Our next step is to make the script aware of the desired temperature. As mentioned in the previous post, if you don’t have a way to control the air flow between rooms, then a single entity for target temperature will be sufficient; we’ll still let the script accept a list of entities for the target temperature, and you can just provide a list of one.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fields:
  room_target_temp_entities:
    name: Room Target Temperature Entities
    description: >-
      A collection of entities which contain the target temperatures for
      each of the rooms. This can be a single desired temp (e.g. "cool to 78")
      or it can be a list (e.g. "cool this room to 75, that room to 78")
      example: "['input_number.office_target_temperature', 'input_number.bedroom_target_temperature']"
    required: true
    selector:
      entity:
        filter:
          - domain: input_number
        multiple: true

And now we’ll do a similar treatment where we get a single target temperature from the list of target temperatures which is provided to the script; again, we’ll do this in the variables block

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
variables:
  # Gather a list of the temperatures for all sensors
  all_target_temps: >
    {{ room_target_temp_entities | map('states') | select('match', '^[0-9.]+$') |  map('int') | list }}

  # Determine the temp to use as the target temp; min temp if cooling, max temp
  # for heating.
  current_target_temp: >-
    {% if current_hvac_mode == "cool" %}
      {{ all_target_temps | min }}
    {% else %}
      {{ all_target_temps | max }}
    {% endif %}

Decide on a course of action

At this point, we’re mostly done with the hard stuff; the rest is pretty straightforward. Now that we have the current temperature and desired temperature, we need to determine what to do with it. In this, we create the first action of the script’s sequence, which is to test the target vs the current temperatures and see if action is warranted.

In the next step, we’ll be getting ready to define the actual temperature we want to send to the thermostat, but in this step we’re simply gathering three pieces of information based on which mode we’re in, heating or cooling. The three pieces of information are:

  • Whether or not the HVAC system should start running (or, if already running, continue to run)
  • What temperature we should target if we should not run
  • What temperature we should target if we should run

In this case, “turning off the AC” is not actually telling the thermostat to go into “off” mode; instead we’ll be setting the thermostat to a temperature higher than the current temp. Conversely, if we want the AC to turn on, then we’ll send a temperature which is lower than the current temp. As an example, if the current temperature is 77 and we want the AC to stop, we’ll send a temperature of, say, 79. Otherwise, if we want it to start running, we’ll set the AC to target 75. The former will cause the unit to stop running, the latter will cause the unit to start running.

To help this, we’re going to add a buffer to the temperature so we don’t get caught cycling very fast when the temperature is right on the threshold. We’ll also add an overshoot value which will help guarantee the desired operation of our system.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
fields:
  buffer:
    name: Buffer
    description: >-
      A buffer applied to the temperature when reading which helps prevent
      rapid cycling
    example: "0.5"
    default: 0.5
    selector:
      number:
        min: 0
        max: 2
        step: 0.1
        mode: box

  overshoot:
    name: Overshoot
    description: >-
      This will be added/subtracted to the target temperature in order to
      ensure the thermostat stays engaged until next iteration of this script
    example: "2"
    default: 2
    selector:
      number:
        min: 0
        max: 3
        step: 0.1
        mode: box

And we’ll use them in the action step in the following way. The buffer will be added to the target temperature (in cooling mode; subtracted otherwise) when testing; so if the buffer is 1 and the target temperature is 75, we’ll check to see if the current temperature is over 75 + 1 or 76 degrees; this will save a little energy and help to prevent the AC from cycling on and off more rapidly. As for the overshoot, when we set the target temperature to the thermostat, assuming we have an overshoot of 3 degrees, when we want to turn on the thermostat set to 75, we’ll instead set it to 75 - 3 or 72 degrees; if we want to turn off the thermostat, we’ll instead use 75 + 3 or 78 degrees. This way, the HVAC system engages longer.

We set these values in the following action:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
sequence:
  - alias: Decide whether we should engage the HVAC and set the target temp
    if:
      - alias: Test to see if we're in cooling mode
        condition: template
        value_template: >-
          {{ current_hvac_mode == "cool" }}
    then:
      - alias: Set the variables for cooling
        variables:
          should_engage: >-
            {{ current_aggregate_temp > current_target_temp + buffer }}
          target_engaged_temp: >-
            {{ current_target_temp - overshoot }}
          target_disengaged_temp: >-
            {{ current_thermostat_temp + overshoot }}
    else:
      - alias: Set the variables for heating
        variables:
          should_engage: >-
            {{ current_aggregate_temp < current_target_temp - buffer }}
          target_engaged_temp: >-
            {{ current_target_temp + overshoot }}
          target_disengaged_temp: >-
            {{ current_thermostat_temp - overshoot }}

Select the appropriate target temperature

Now that we have possible target temperatures and know whether or not we should engage, we build a new action which decides upon the correct target temperature and sets it to a variable which will be used soon. We do this in stages because it’s easier to debug and reason about what is going on if you break things down into multiple steps

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
sequence:
  - alias: Choose the target temp as appropriate based on mode and temp
    if:
      - alias: Test to see if we need to engage the thermostat
        condition: template
        value_template: >-
          {{ should_engage }}
    then:
      - alias: Use the engage values for the target
        variables:
          actual_target_temp: "{{ target_engaged_temp }}"
    else:
      - alias: Use the disengaged value for the target
        variables:
          actual_target_temp: "{{ target_disengaged_temp }}"

Send the temperature to the thermostat, if appropriate

Before we actually send the temperature (actual_target_temp from above) to the thermostat, we should see if the system is in the right mode for this. There’s two things which could be preventing our setting of the thermostat: if the system is disabled (meaning, if the binary sensor we created in the last post is turned on) and if thermostat is not in the right mode:

1
2
3
4
5
6
7
8
9
10
11
12
sequence:
  - alias: Check to see if the HVAC system is disabled; don't continue if it's not
    condition: state
    entity_id: binary_sensor.disable_upstairs_climate_operation
    state: "off"

  - alias: Check to see if the thermostat is on; don't continue if it's not
    condition: template
    value_template: >-
      {{ is_state(thermostat, "heat") or is_state(thermostat, "cool") }}

These conditions only let the logic flow past if they are true; thus if either is false, the execution stops right now. But, should these conditions pass, we can finally do the last piece of logic:

1
2
3
4
5
6
7
8
9
10
    sequence:
      - alias: Update the thermostat's temperature
        action: climate.set_temperature
        target:
          entity_id: >-
            {{ thermostat }}
        data:
          temperature: "{{ actual_target_temp }}"

Congrats! That was a lot, but that’s because it does a lot; you can see the whole script here.

Scheduling

Short cycling prevention

Short cycling is when the system turns on and off too rapidly. Not only will this not adequately manage the temperature, this can greatly reduce the life of the system. Thus, one of the first steps we’ll take is to avoid this. The simplest way (and the way we’re going to use) is to not update the target temperature too frequently; in our case, we’ll run an update every 15 minutes.

To schedule this, we’ll create an automation which will execute the script we just described. As mentioned before, we are separating the script from the scheduling so we can execute the script from multiple different triggers, as well as to be able to assign different conditions for each.

The automation itself is pretty straightforward, so I’ll just touch on the various parts

Triggers

Not only do we want to make this happen every 15 minutes, but we also want it to run at other times, like when the upstairs HVAC is no longer disabled (e.g. when the windows are closed) or when we restart home assistant; that way, we don’t have to wait for the next execution.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
triggers:
  - alias: Every 15 minutes
    trigger: time_pattern
    minutes: "/15"

  - alias: Upon restarting Home Assistant
    trigger: homeassistant
    event: start

  - alias: When the windows are closed
    trigger: state
    entity_id: binary_sensor.disable_upstairs_climate_operation
    to: "off"
    for:
      seconds: 10

Conditions

We only want the script to run if we’re likely to be updating the temperature, so let’s do some quick sanity checks first

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
conditions:
  - alias: HVAC is in automatic mode
    condition: state
    entity_id: input_boolean.hvac_manual_mode
    state: "off"

  - alias: We're currently in cooling or heating mode; we don't support others
    condition: state
    entity_id: input_select.hvac_mode
    state:
      - "cool"
      - "heat"

  - alias: The HVAC is not currently disabled due to windows being open
    condition: state
    entity_id: binary_sensor.disable_upstairs_climate_operation
    state: "off"

Calling the script

Finally, we can call the script, passing in values for the fields we defined above as appropriate. This is what it looks like for mine; note that any field not passed as part of the data object is set to the default value defined in the script fields

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
actions:
  - alias: Call the script to update the thermostat
    action: script.update_thermostat_temp
    data:
      thermostat_temp_sensor: sensor.upstairs_thermostat_home_kit_temperature
      thermostat: climate.upstairs_thermostat_home_kit
      room_temp_sensors:
        # it would be good to convert this into a group
        - sensor.cora_room_temperature_friendly
        - sensor.office_temperature_friendly
        - sensor.guest_room_temperature_friendly
        - sensor.main_bedroom_temperature_friendly
      room_target_temp_entities:
        # it would be good to convert this into a group
        - input_number.cora_bedroom_target_temp
        - input_number.game_room_target_temp
        - input_number.main_bedroom_target_temp
        - input_number.office_target_temp
        - input_number.upstairs_base_target_temp
      current_hvac_mode: cool

Putting it all together

You can find the full script here

Wrap up

Well, that was a lot! If you made it this far, congrats! As you can see, this is a pretty complex set of instructions, but then it is a fairly complex system that we’re dealing with here. Most of this already happens under the hood inside a modern thermostat; it’s just that I want a bit more control over this and more ability to extend out to using the Flair system. With that said, I’ve had this running in my house all summer and it works quite well… or, at least, it did on the few days where it got hot enough to actually trigger the HVAC; it’s been quite mild this summer, thankfully.

As for the Flair integration I mentioned: I’m still working on that. Since the summer has been nice, I haven’t felt compelled to integrate that quite yet. However, I’m sure it’ll hot up towards the beginning of autumn, urging me to make the improvements and get everything working together. I’ll share my configuration for that once I get to that point.

That’s it for now; thanks for reading!

YAML configurations

Update Thermostat Script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
script:
  update_thermostat_temp:
    alias: Climate - Update Thermostat Temperature
    mode: restart
    description: >-
      This script handles checking the current temperatures, the desired temperature,
      and the HVAC mode (all passed into this script) and then instructs the
      HVAC system to turn on/off accordingly by setting the target temperature
      of the thermostat in charge.

      This script expects to work with a single thermostat; if you have multiple
      thermostats, set up an automation for each.

      This script only anticipates working with an enabled HVAC system, meaning
      in "cool" or "heat" mode. Currently, it does not work with "heat_cool".
      If the HVAC system is in "off" mode, then do not call this script. It also
      does not pay any attention to away mode; that should be handled separately,
      perhaps as a condition in the automation.

    fields:
      buffer:
        name: Buffer
        description: >-
          A buffer applied to the temperature when reading which helps prevent
          rapid cycling
        example: "0.5"
        default: 0.5
        selector:
          number:
            min: 0
            max: 2
            step: 0.1
            mode: box

      overshoot:
        name: Overshoot
        description: >-
          This will be added/subtracted to the target temperature in order to
          ensure the thermostat stays engaged until next iteration of this script
        example: "2"
        default: 2
        selector:
          number:
            min: 0
            max: 3
            step: 0.1
            mode: box

      thermostat_temp_sensor:
        name: Thermostat Temperature Sensor
        description: >-
          The temperature the thermostat currently thinks it is. This can often
          be different than the temperature in multiple rooms, yet controls the
          real-world behavior of the system, so we need to include it
        example: "sensor.my_thermostat_temperature"
        required: true
        selector:
          entity:
            filter:
              - device_class: temperature
                domain: sensor

      thermostat:
        name: Thermostat
        description: The thermostat which should be controlled by this script
        example: "climate.my_thermostat"
        required: true
        selector:
          entity:
            filter:
              - domain: climate

      room_temp_sensors:
        name: Room Temperature Sensors
        description: >-
          A collection of room temperatures which should be considered when
          calculating whether to engage or disengage the HVAC system. These are
          likely to be temp sensors in the rooms you want covered.
        example: "['sensor.office_temperature', 'sensor.bedroom_temperature']"
        required: true
        selector:
          entity:
            filter:
              - device_class: temperature
                domain: sensor
            multiple: true

      room_target_temp_entities:
        name: Room Target Temperature Entities
        description: >-
          A collection of entities which contain the target temperatures for
          each of the rooms. This can be a single desired temp (e.g. "cool to 78")
          or it can be a list (e.g. "cool this room to 75, that room to 78")
        example: "['input_number.office_target_temperature', 'input_number.bedroom_target_temperature']"
        required: true
        selector:
          entity:
            filter:
              - domain: input_number
            multiple: true

      current_hvac_mode:
        name: Current HVAC Mode
        description: >-
          The mode which the HVAC system is currently in. Accepted values are
          "cool" and "heat".
        example: cool
        required: true
        selector:
          select:
            options:
              - cool
              - heat

    variables:
      buffer: "{{ buffer | default(0.5) | float }}"
      overshoot: "{{ overshoot | default(2) | float }}"

      # add the thermostat temp sensor to the list of room temp sensors
      all_temp_sensors: >
        {{ room_temp_sensors + [thermostat_temp_sensor] }}

      # Gather the temperatures for all the sensors
      all_current_temps: >
        {{ all_temp_sensors | map('states') | select('match', '^[0-9.]+$') |  map('int') | list }}

      # Gather a list of the temperatures for all sensors
      all_target_temps: >
        {{ room_target_temp_entities | map('states') | select('match', '^[0-9.]+$') |  map('int') | list }}

      # We'll also need the thermostat current temp for future use
      current_thermostat_temp: "{{ states(thermostat_temp_sensor) | int }}"

      # Determine the temp to use as the current temp; max temp if cooling, min temp
      # for heating. More description: if we're heating, we want to make the
      # coldest room the current temp so that can be heated to the desired temp.
      # For cooling, we want to cool the hottest room to the target temp
      current_aggregate_temp: >-
        {% if current_hvac_mode == "cool" %}
          {{ all_current_temps | max }}
        {% else %}
          {{ all_current_temps | min }}
        {% endif %}

      # Determine the temp to use as the target temp; min temp if cooling, max temp
      # for heating.
      current_target_temp: >-
        {% if current_hvac_mode == "cool" %}
          {{ all_target_temps | min }}
        {% else %}
          {{ all_target_temps | max }}
        {% endif %}

    sequence:
      - alias: Decide whether we should engage the HVAC and set the target temp
        if:
          - alias: Test to see if we're in cooling mode
            condition: template
            value_template: >-
              {{ current_hvac_mode == "cool" }}
        then:
          - alias: Set the variables for cooling
            variables:
              should_engage: >-
                {{ current_aggregate_temp > current_target_temp + buffer }}
              target_engaged_temp: >-
                {{ current_target_temp - overshoot }}
              target_disengaged_temp: >-
                {{ current_thermostat_temp + overshoot }}
        else:
          - alias: Set the variables for heating
            variables:
              should_engage: >-
                {{ current_aggregate_temp < current_target_temp - buffer }}
              target_engaged_temp: >-
                {{ current_target_temp + overshoot }}
              target_disengaged_temp: >-
                {{ current_thermostat_temp - overshoot }}

      - alias: Choose the target temp as appropriate based on mode and temp
        if:
          - alias: Test to see if we need to engage the thermostat
            condition: template
            value_template: >-
              {{ should_engage }}
        then:
          - alias: Use the engage values for the target
            variables:
              actual_target_temp: "{{ target_engaged_temp }}"
        else:
          - alias: Use the disengaged value for the target
            variables:
              actual_target_temp: "{{ target_disengaged_temp }}"

      - alias: Check to see if the HVAC system is disabled; don't continue if it's not
        condition: state
        entity_id: binary_sensor.disable_upstairs_climate_operation
        state: "off"

      - alias: Check to see if the thermostat is on; don't continue if it's not
        condition: template
        value_template: >-
          {{ is_state(thermostat, "heat") or is_state(thermostat, "cool") }}

      - alias: Update the thermostat's temperature
        action: climate.set_temperature
        target:
          entity_id: >-
            {{ thermostat }}
        data:
          temperature: "{{ actual_target_temp }}"

Scheduling Automation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
automation:
  - id: climate_automation_upstairs
    alias: Climate - Upstairs Control
    description: >-
      This is the main control script for the HVAC system upstairs. The main
      idea is that, upon every trigger, the target temp is calculated, the
      aggregate current temp is calculated, and the set point of the thermostat
      is updated accordingly.

      The target temp is the lowest of all the currently set room temperatures
      plus an offset; the offset is negative if cooling, positive if heating. By
      setting the temp beyond the actual target, we will prevent the possibility
      of rapidly cycling the HVAC system, which is bad.

      The aggregate current temp is the most extreme of all the rooms in which
      there is a temp sensor; the extreme is the highest if cooling and lowest
      if heating.

    triggers:
      - alias: Every 15 minutes
        trigger: time_pattern
        minutes: "/15"

      - alias: Upon restarting Home Assistant
        trigger: homeassistant
        event: start

      - alias: When the windows are closed
        trigger: state
        entity_id: binary_sensor.disable_upstairs_climate_operation
        to: "off"
        for:
          seconds: 10

      # Add this eventually
      # - alias: Upon manually changing the temperature

      # Add this eventually
      # - alias: Upon switching to guest mode

    conditions:
      - alias: HVAC is in automatic mode
        condition: state
        entity_id: input_boolean.hvac_manual_mode
        state: "off"

      - alias: We're currently in cooling or heating mode; we don't support others
        condition: state
        entity_id: input_select.hvac_mode
        state:
          - "cool"
          - "heat"

      - alias: The HVAC is not currently disabled due to windows being open
        condition: state
        entity_id: binary_sensor.disable_upstairs_climate_operation
        state: "off"

      # Add this eventually
      # - alias: HVAC is not in temporary override mode

    actions:
      - alias: Call the script to update the thermostat
        action: script.update_thermostat_temp
        data:
          thermostat_temp_sensor: sensor.upstairs_thermostat_home_kit_temperature
          thermostat: climate.upstairs_thermostat_home_kit
          room_temp_sensors:
            # it would be good to convert this into a group
            - sensor.cora_room_temperature_friendly
            - sensor.office_temperature_friendly
            - sensor.guest_room_temperature_friendly
            - sensor.main_bedroom_temperature_friendly
          room_target_temp_entities:
            # it would be good to convert this into a group
            - input_number.cora_bedroom_target_temp
            - input_number.game_room_target_temp
            - input_number.main_bedroom_target_temp
            - input_number.office_target_temp
            - input_number.upstairs_base_target_temp
          current_hvac_mode: cool

This post is licensed under CC BY 4.0 by the author.