Personal setup: weather-adaptive, floor-aware climate control

I’ve been working on a personal whole-home climate setup in HA that replaces most of the “smart” logic in my Nest thermostats. My house runs on Nests, which are great for scheduling and pre-heating/cooling, but I could never get the actual “feels like” comfort right. Setting the house to 70º when it’s in the 70s outside is totally different from when it’s in the 40s. I found myself adjusting it more than I expected. Radiant is slow, AC is fast, and I don’t want them fighting each other. I also wanted a true set-and-forget system plus some per-floor nuance. Home Assistant seemed perfect for that.


House Setup

Three floors, radiant heat on all three, AC only on the upper two, and each floor behaves differently (size, layout, usage, schedule, drafts, sun exposure, etc.). There’s also an office with no radiant heat and weak AC, so it’s controlled separately with a convection heater and a flakey temperature sensor—but I’m skipping that here to keep this post simpler.

Design Goals

  • One consistent comfort baseline for the whole house
  • Automatically adjust for outdoor temperature
  • Each floor layers small usage-based offsets on top
  • Avoid big swings, stick to gentle, time-based nudges
  • HA computes the target temperature, Nest continues to executes it
  • Make the whole system readable and debuggable at a glance

Solution

(Screenshot at the bottom shows an example with actual temp values and targets.)

I broke this into 3 layers:

Layer 1: Outdoor-Driven Comfort Offset

Instead of relying on static schedules, HA reacts to weather. I pull outdoor temperatures from a nearby station and compute a 3-day average to decide whether we’re in a “cold” or “warm” period. A simple curve maps outdoor temps to an offset (0 to +3 in heating season, 0 to −3 in cooling season). Example:

Cold outside = 3-day mean < 65º
Offset = +1 for 45–65º, +2 for <45º, etc. (cooling has its own curve)

Automation #1 – Cold Outside
alias: Climate — Toggle Cold Outside
description: ""
triggers:
  - trigger: state
    entity_id:
      - sensor.outside_temp_3d_mean
conditions: []
actions:
  - variables:
      avg: "{{ states('sensor.outside_temp_3d_mean')|float(60) }}"
  - choose:
      - conditions:
          - condition: template
            value_template: "{{ avg <= 57 }}"
        sequence:
          - target:
              entity_id: input_boolean.cold_outside
            action: input_boolean.turn_on
      - conditions:
          - condition: template
            value_template: "{{ avg >= 62 }}"
        sequence:
          - target:
              entity_id: input_boolean.cold_outside
            action: input_boolean.turn_off
mode: restart

Layer 2: House Target Temperature

I defined an input helper to set the base comfort temperature. That represents my desired “feels like” temperature. The global house target temperature is calculated based on that base temperature plus that outdoor-temperature offset. This keeps the whole house generally a little warmer on cold days and a little cooler on hot ones. Example:

Base 70º, Outside 48º → Offset +1 → Target = 71º

I’m also accounting for when I’m on vacation and clamping minor fluctuations to avoid big swings.

Automation #2 – House Target Temperature
alias: Climate — House Target Temperature
description: ""
triggers:
  - trigger: time_pattern
    minutes: /30
  - trigger: state
    entity_id:
      - input_boolean.cold_outside
      - input_number.house_base_temperature
      - input_select.home_presence
conditions: []
actions:
  - variables:
      outside: >-
        {{ states('sensor.weather_station_feels_like')|float(60)
        }}
      is_cold: "{{ is_state('input_boolean.cold_outside', 'on') }}"
      offset: |
        {% if is_cold %}
          {% if outside < 35 %} 3
          {% elif outside < 45 %} 2
          {% elif outside < 65 %} 1
          {% else %} 0
          {% endif %}
        {% else %}
          {% if outside > 88 %} -3
          {% elif outside > 82 %} -2
          {% elif outside > 76 %} -1
          {% else %} 0
          {% endif %}
        {% endif %}
      target: |
        {% if is_state('input_select.home_presence', 'Vacation') %}
          {% if is_cold %}
            66
          {% else %}
            78
          {% endif %}
        {% else %}
          {{ states('input_number.house_base_temperature')|float + offset|float }}
        {% endif %}
      current: "{{ states('input_number.house_target_temperature')|float(0) }}"
      diff: "{{ (target|float) - current }}"
  - condition: template
    value_template: "{{ diff | abs >= 0.9 }}"
  - action: input_number.set_value
    target:
      entity_id: input_number.house_target_temperature
    data:
      value: "{{ target }}"
mode: restart

Layer 3: Per-Floor Behavior

I created automations for each floor. Think tiny nudges (like +1 in the morning, or 0 at night) based on how that space is actually used and time of day. Example:

Upstairs:

  • 05:00–06:00 – Morning pre-warm (+1º)
  • 06:00–20:30 – Day baseline (0º)
  • 20:00–21:00 – Evening warm-up (+1º)
  • 21:00–05:00 – Night setback (-1º)

Floors with heating and cooling get a derived heat/cool set points (range). The one floor with just radiant heat gets a single set point. Each zone ends up feeling “right” at the right time without ever pushing radiant into big temperature swings.

Automation #3 – Upstairs
alias: Climate — Upstairs
description: ""
triggers:
  - trigger: state
    entity_id:
      - input_number.house_target_temperature
      - input_select.home_presence
    for:
      seconds: 3
  - trigger: time
    at: "05:30:00"
  - trigger: time
    at: "07:30:00"
  - trigger: time
    at: "20:00:00"
  - trigger: time
    at: "21:30:00"
  - trigger: time_pattern
    minutes: /30
conditions: []
actions:
  - variables:
      offset: 0
  - choose:
      - conditions:
          - condition: state
            entity_id: input_select.home_presence
            state: Vacation
        sequence:
          - variables:
              offset: 0
      - conditions:
          - condition: time
            after: "05:30:00"
            before: "07:30:00"
        sequence:
          - variables:
              offset: 1
      - conditions:
          - condition: time
            after: "07:30:00"
            before: "20:00:00"
        sequence:
          - variables:
              offset: 0
      - conditions:
          - condition: time
            after: "20:00:00"
            before: "21:00:00"
        sequence:
          - variables:
              offset: 0
      - conditions:
          - condition: or
            conditions:
              - condition: time
                after: "21:00:00"
              - condition: time
                before: "05:30:00"
        sequence:
          - variables:
              offset: -1
    default:
      - variables:
          offset: 0
  - variables:
      house: "{{ states('input_number.house_target_temperature')|float }}"
      is_cold: "{{ is_state('input_boolean.cold_outside','on') }}"
      current: "{{ state_attr('climate.upstairs','current_temperature')|float }}"
      desired: "{{ house + offset }}"
      target: |
        {% if is_cold %}
          {{ desired }}
        {% else %}
          {% if current < desired %}
            {{ house }}
          {% else %}
            {{ desired }}
          {% endif %}
        {% endif %}
      new_low: |
        {% if is_cold %}
          {{ target }}
        {% else %}
          {{ (target - 3)|round(1) }}
        {% endif %}
      new_high: |
        {% if is_cold %}
          {{ (target + 3)|round(1) }}
        {% else %}
          {{ target }}
        {% endif %}
      current_low: "{{ state_attr('climate.upstairs','target_temp_low')|float(0) }}"
      current_high: "{{ state_attr('climate.upstairs','target_temp_high')|float(0) }}"
  - condition: template
    value_template: |
      {{ (new_low - current_low)|abs >= 0.4
         or (new_high - current_high)|abs >= 0.4 }}
  - action: climate.set_temperature
    target:
      entity_id: climate.upstairs
    data:
      target_temp_low: "{{ new_low }}"
      target_temp_high: "{{ new_high }}"
mode: restart

I’m being explicit with redundant choose cases to make reading automation traces easier. Some floors have an additional condition to suppress heating if the current temperature exceeds a certain value. The Office has more conditions based on location and whether I’m working from home or from the office.

Debugging panel

I have a markdown card that explains, in plain English, what each room is targeting and why. It’s been incredibly useful for debugging and tuning. I still update it manually when I tweak automations, but I can figure out something more clever later. I’ve also added quick toggles to enable/disable each floor’s automations in case I want to apply a quick override to the set points.


Results

So far it’s been really stable; predictable warming, and way more control than Nest’s built-in logic alone. I still get the occasional overshoot because I’m still learning how much time it takes for radiant heat to heat each space. So I’m still fine-tuning timings and offsets for each floor, but so far it’s been a huge upgrade. Obviously, I haven’t tested summer behavior yet, and I expect to do some further tweaking when that happens.

Happy to answer questions. Would love to hear feedback or smarter ideas to evolve this!

1 Like

Cool idea. Can you add card config

Here you go. I’m using Bubble card and heavy card styling.

type: vertical-stack
cards:
  - type: custom:bubble-card
    card_type: pop-up
    name: Weather & Climate
    icon: mdi:thermometer
    hash: "#climate"
    button_type: name
    bg_color: "#121212"
    styles: |-
      .bubble-name {
        font-size: larger;
      }
  - type: custom:bubble-card
    card_type: button
    button_type: state
    name: " "
    entity: weather.openweathermap
    card_layout: large-2-rows
    show_icon: true
    force_icon: false
    sub_button:
      - show_state: true
        icon: ""
        state_background: false
        entity: sensor.weather_station_feels_like
        show_background: false
        show_attribute: true
        show_name: false
        show_last_changed: false
      - show_state: true
        state_background: false
        entity: sensor.weather_station_humidity
        show_background: false
        show_attribute: true
        show_name: false
        show_icon: true
        show_last_changed: false
      - show_state: true
        state_background: false
        entity: sensor.aqi
        show_background: false
        show_attribute: true
        show_name: false
        show_icon: true
        show_last_changed: false
      - show_state: true
        state_background: false
        icon: mdi:cloud-percent
        entity: sensor.openweathermap_cloud_coverage
        show_background: false
        show_attribute: true
        show_name: false
        show_icon: true
        show_last_changed: false
    styles: >-
      .bubble-background { border-radius: 8px !important; background:
      transparent; } 

      .bubble-container {
        border-radius: 8px !important; 
        background: var(--card-background-color);
        background: transparent;
        # background: var(--primary-text-color);
        # background: #DDD;
        # color: var(--primary-background-color);
        margin: 10px 4px 4px;
        height: 70px !important;
      } 

      .bubble-icon-container {
        background: color-mix(in srgb, ${
            hass.states['binary_sensor.dark_outside'].state == 'om' ? 
              'var(--orange-color)' : 'var(--teal-color)'
          }, black 50%);
        background: var(--card-background-color);
        background: var(--secondary-background-color);
        background: transparent;
        height: 50px;
        margin: 12px !important;
      }

      .is-on .bubble-icon-container { background: var(--card-background-color);
      }

      .bubble-range-slider { border-radius: 8px !important; }

      .bubble-range-fill { background: var(--accent-color) !important; }

      .bubble-name { font-weight: var(--ha-font-weight-normal);
        font-size: var(--ha-font-size-2xl);
        line-height: var(--ha-line-height-condensed);
      }

      .bubble-state { 
        opacity: .7;
        font-weight: var(--ha-font-weight-normal);
        font-size: var(--ha-font-size-m);
      }

      .bubble-entity-picture {
        background-image: url(${hass.states['sensor.openweathermap_weather_icon'].state});
        background-size: cover;
        display: unset !important;
        transform: scale(1.5);
        transform-origin: center center;
      }

      .bubble-sub-button {
        # display: none;
      } .bubble-icon {
        display: none;
      } .bubble-sub-button-container {
        gap: 0;
      } .bubble-sub-button-1, .bubble-sub-button-2, .bubble-sub-button-3,
      .bubble-sub-button-4, .bubble-sub-button-5 {
        font-size: var(--ha-font-size-s);
        font-weight: var(--ha-font-weight-normal);
        padding: 0 8px 0 2px;
        # color: var(--primary-background-color);
      } .bubble-sub-button-1 > ha-icon {
        color: ${
          hass.states['sensor.weather_station_feels_like'].state >= 90 ?
            'var(--red-color)' : 
          hass.states['sensor.weather_station_feels_like'].state >= 75 ?
            'var(--orange-color)' : 
          hass.states['sensor.weather_station_feels_like'].state >= 60 ?
            'var(--green-color)' : 
          hass.states['sensor.weather_station_feels_like'].state >= 45 ?
            'var(--teal-color)' : 
          'var(--blue-color)' } !important;
      } .bubble-sub-button-2 > ha-icon {
        color: ${
          hass.states['sensor.weather_station_humidity'].state >= 81 ?
            'var(--purple-color)' : 
          hass.states['sensor.weather_station_humidity'].state >= 66 ?
            'var(--blue-color)' : 
          hass.states['sensor.weather_station_humidity'].state >= 51 ?
            'var(--teal-color)' : 
          hass.states['sensor.weather_station_humidity'].state >= 31 ?
            'var(--green-color)' : 
          'var(--yellow-color)' } !important;
      } .bubble-sub-button-4 > ha-icon {
        color: ${
          hass.states['sensor.openweathermap_cloud_coverage'].state >= 71 ?
            'var(--disabled-color)' : 
          hass.states['sensor.openweathermap_cloud_coverage'].state >= 51 ?
            'var(--dark-grey-color)' : 
          hass.states['sensor.openweathermap_cloud_coverage'].state >= 31 ?
            'var(--grey-color)' : 
          hass.states['sensor.openweathermap_cloud_coverage'].state >= 11 ?
            'var(--light-gray-color)' : 
          'var(--light-blue-color)' } !important;
      } .bubble-sub-button-3 > ha-icon {
        color: ${hass.states['sensor.aqi'].attributes.color} !important;
      } ${card.querySelector('.bubble-name').innerText = 
        hass.formatEntityState(hass.states['weather.openweathermap']).replace(', night','') }
      ${card.querySelector('.bubble-state').innerText = 
        hass.formatEntityState(hass.states['sensor.openweathermap_weather']).charAt(0).toUpperCase() +
        hass.formatEntityState(hass.states['sensor.openweathermap_weather']).substr(1) }
    scrolling_effect: false
  - type: conditional
    conditions:
      - condition: state
        entity: binary_sensor.rain_soon
        state: "on"
    card:
      type: custom:mushroom-template-card
      primary: >-
        Rain expected in the next {{
        state_attr('binary_sensor.rain_soon','window_hours') }} hours
      hold_action:tap     hold_action:
        action: navigate
        navigation_path: "#climate"
      grid_options:
        columns: full
      secondary: Are the patio cushions covered?
      icon: mdi:weather-pouring
      color: white
      card_mod:
        style: |
          ha-card {
            background: color-mix(in oklch, var(--blue-color) 70%, black 70%);
          }
  - type: custom:vertical-stack-in-card
    cards:
      - type: custom:mushroom-template-card
        primary: >-
          {{ 'Today' if state_attr(entity,'forecast')[0].is_daytime else
          'Tonight' }} → {{
          state_attr('sensor.nws_twice_daily_forecast','forecast')[0].short_description
          }}
        secondary: "{{ states('sensor.nws_twice_daily_forecast') }}"
        tap_action:
          action: navigate
          navigation_path: "#climate"
        multiline_secondary: true
        entity: sensor.nws_twice_daily_forecast
        features_position: bottom
        card_mod:
          style:
            .: |-
              ha-card {
                background: none;
                padding: 0 4px !important;
                border-top: 2px solid var(--primary-background-color) !important;
                line-height: 1.2rem;
              }
            mushroom-state-info$: |-
              .primary { 
                margin-top: -2px;
              }
              .secondary { 
                opacity: .7;
              }
      - type: custom:mushroom-template-card
        primary: >-
          {{ 'Tonight' if state_attr(entity,'forecast')[0].is_daytime else
          'Tomorrow' }}

          → {{
          state_attr('sensor.nws_twice_daily_forecast','forecast')[1].short_description
          }}
        secondary: >-
          {{
          state_attr('sensor.nws_twice_daily_forecast','forecast')[1].detailed_description
          }}
        tap_action:
          action: navigate
          navigation_path: "#climate"
        multiline_secondary: true
        entity: sensor.nws_twice_daily_forecast
        card_mod:
          style:
            .: |-
              ha-card {
                background: none;
                padding: 0 4px !important;
                border-top: 2px solid var(--primary-background-color) !important;
                line-height: 1.2rem;
              }
            mushroom-state-info$: |-
              .primary { 
                # margin-top: -2px;
                color: grey !important;
                color: var(--dark-grey-color) !important;
              }
              .secondary { 
                # opacity: .7;
                color: grey !important;
                color: var(--dark-grey-color) !important;
              }
  - type: custom:bubble-card
    card_type: separator
    name: Climate Control
    sub_button: []
    icon: ""
  - type: custom:vertical-stack-in-card
    cards:
      - type: tile
        entity: input_number.house_base_temperature
        name: Base Temp
        icon: mdi:home-thermometer
        color: dark-grey
        hide_state: true
        vertical: false
        features:
          - type: numeric-input
            style: buttons
        features_position: inline
        card_mod:
          style: |
            ha-card {
              background: color-mix(in oklch, var(--blue-color) 30%, black 70%);
              background: var(--primary-card-background);
            }
      - type: custom:bubble-card
        card_type: climate
        entity: climate.upstairs
        icon: fas:u
        show_state: false
        show_attribute: true
        attribute: hvac_action
        show_last_changed: false
        show_icon: true
        hide_target_temp_low: true
        hide_target_temp_high: true
        button_action:
          tap_action:
            action: more-info
        styles: >-
          .bubble-background,

          .bubble-container,

          .bubble-range-slider {
            border-radius: 8px !important;
            border-radius: 0 !important; 
            border-bottom-left-radius: 0px !important;
            border-bottom-right-radius: 0px !important;
            background: var(--card-background-color) !important;
          }

          .bubble-container {
            border-top: 2px solid var(--primary-background-color);
          } .is-on .bubble-icon-container {
            background: ${
              hass.states[entity].attributes.hvac_action === 'cooling' ? 'var(--blue-color)':
              hass.states[entity].attributes.hvac_action === 'heating' ? 'var(--orange-color)':
              'none'
            } !important;
          }

          .bubble-icon {
            transform: scale(.9);
            color:  ${
              hass.states[entity].attributes.hvac_action != 'idle' ? 'var(--primary-background-color)': 'inherit'
            } !important;
            var(--primary-background-color) !important;
          }

          .bubble-range-fill {
            background: var(--accent-color) !important;
          }

          .bubble-name {
            font-weight: var(--ha-font-weight-medium); font-size: var(--ha-font-size-m);
          }

          .bubble-sub-button {
            # font-weight: bold;
            font-size: var(--ha-font-size-l);a-font-size-l);
            padding: 1px 9px 0 12px;
          }

          .bubble-temperature-container {
            display: none;
          }

          .bubble-sub-button-3 > ha-icon {
            color: ${hass.states['automation.climate_upstairs_comfort_control'].state == 'on' ? 
              'var(--orange-color)' : 'var(--dark-grey-color)' } !important; }
          ${subButtonIcon[0].setAttribute("icon",
          hass.states['automation.climate_upstairs_comfort_control'].state ===
          'on' ?
            'mdi:check-bold' : 'mdi:close-thick')}
        sub_button:
          - entity: climate.upstairs
            show_attribute: true
            attribute: current_temperature
            show_icon: false
            state_background: true
            show_background: false
            show_state: false
            show_last_changed: false
          - entity: climate.upstairs
            show_attribute: true
            attribute: current_humidity
            show_icon: false
            state_background: false
            show_background: false
          - entity: automation.climate_upstairs_comfort_control
            tap_action:
              action: toggle
            state_background: false
            icon: mdi:check-bold
      - type: custom:bubble-card
        card_type: climate
        entity: climate.office
        icon: fas:o
        show_state: false
        show_attribute: true
        attribute: hvac_action
        show_last_changed: false
        show_icon: true
        hide_target_temp_low: true
        hide_target_temp_high: true
        button_action:
          tap_action:
            action: more-info
        styles: >-
          .bubble-background,

          .bubble-container,

          .bubble-range-slider {
            border-radius: 0 !important; 
            background: var(--card-background-color) !important;
          }

          .bubble-container {
            border-top: 2px solid var(--primary-background-color);
          }

          .is-on .bubble-icon-container {
            background: ${
              hass.states[entity].attributes.hvac_action === 'cooling' ? 'var(--blue-color)':
              hass.states[entity].attributes.hvac_action === 'heating' ? 'var(--orange-color)':
              'none'
            } !important;
          }

          .bubble-icon {
            transform: scale(.9);
            color:  ${
              hass.states[entity].attributes.hvac_action != 'idle' ? 'var(--primary-background-color)': 'inherit'
            } !important;
            var(--primary-background-color) !important;
          }

          .bubble-range-fill {
            background: var(--accent-color) !important;
          }

          .bubble-name {
            font-weight: var(--ha-font-weight-medium); font-size: var(--ha-font-size-m);
          }

          .bubble-sub-button {
            # font-weight: bold;
            font-size: var(--ha-font-size-l);
            padding: 1px 9px 0 12px;
          }

          .bubble-temperature-container {
            display: none;
          }

          .bubble-sub-button-3 > ha-icon {
            color: ${hass.states['automation.climate_office_comfort_control'].state == 'on' ? 
              'var(--orange-color)' : 'var(--dark-grey-color)' } !important; }
          ${subButtonIcon[0].setAttribute("icon",
          hass.states['automation.climate_office_comfort_control'].state ===
          'on' ?
            'mdi:check-bold' : 'mdi:close-thick')}
        sub_button:
          - entity: climate.office
            show_attribute: true
            attribute: current_temperature
            show_icon: false
            state_background: true
            show_background: false
            show_state: false
            show_last_changed: false
          - entity: sensor.office_temperature_humidity
            show_state: true
            show_icon: false
            state_background: false
            show_background: false
          - entity: automation.climate_office_comfort_control
            tap_action:
              action: toggle
            state_background: false
            icon: mdi:check-bold
      - type: custom:bubble-card
        card_type: climate
        entity: climate.family_room
        icon: fas:m
        show_state: false
        show_attribute: true
        attribute: hvac_action
        show_last_changed: false
        show_icon: true
        hide_target_temp_low: true
        hide_target_temp_high: true
        button_action:
          tap_action:
            action: more-info
        styles: >-
          .bubble-background,

          .bubble-container,

          .bubble-range-slider {
            border-radius: 0 !important; 
            background: var(--card-background-color) !important;
          }

          .bubble-container {
            border-top: 2px solid var(--primary-background-color);
          }

          .is-on .bubble-icon-container {
            background: ${
              hass.states[entity].attributes.hvac_action === 'cooling' ? 'var(--blue-color)':
              hass.states[entity].attributes.hvac_action === 'heating' ? 'var(--orange-color)':
              'none'
            } !important;
          }

          .bubble-icon {
            transform: scale(.9);
            color:  ${
              hass.states[entity].attributes.hvac_action != 'idle' ? 'var(--primary-background-color)': 'inherit'
            } !important;
            var(--primary-background-color) !important;
          }

          .bubble-range-fill {
            background: var(--accent-color) !important;
          }

          .bubble-name {
            font-weight: var(--ha-font-weight-medium); font-size: var(--ha-font-size-m);
          }

          .bubble-sub-button {
            # font-weight: bold;
            font-size: var(--ha-font-size-l);
            padding: 1px 9px 0 12px;
          }

          .bubble-temperature-container {
            display: none;
          }

          .bubble-sub-button-3 > ha-icon {
            color: ${hass.states['automation.climate_family_room_comfort_control'].state == 'on' ? 
              'var(--orange-color)' : 'var(--dark-grey-color)' } !important; }
          ${subButtonIcon[0].setAttribute("icon",
          hass.states['automation.climate_family_room_comfort_control'].state
          === 'on' ?
            'mdi:check-bold' : 'mdi:close-thick')}
        sub_button:
          - entity: climate.family_room
            show_attribute: true
            attribute: current_temperature
            show_icon: false
            state_background: true
            show_background: false
            show_state: false
            show_last_changed: false
          - entity: climate.family_room
            show_attribute: true
            attribute: current_humidity
            show_icon: false
            state_background: false
            show_background: false
          - entity: automation.climate_family_room_comfort_control
            tap_action:
              action: toggle
            state_background: false
            icon: mdi:check-bold
      - type: custom:bubble-card
        card_type: climate
        entity: climate.downstairs
        icon: fas:d
        show_state: false
        show_attribute: true
        attribute: hvac_action
        show_last_changed: false
        show_icon: true
        hide_target_temp_low: true
        hide_target_temp_high: true
        button_action:
          tap_action:
            action: more-info
        styles: >-
          .bubble-background,

          .bubble-container,

          .bubble-range-slider {
            border-radius: 8px !important;
            border-top-left-radius: 0px !important;
            border-top-right-radius: 0px !important;
            background: var(--card-background-color) !important;
          }

          .bubble-container {
            border-top: 2px solid var(--primary-background-color);
          }

          .is-on .bubble-icon-container {
            background: ${
              hass.states[entity].attributes.hvac_action === 'cooling' ? 'var(--blue-color)':
              hass.states[entity].attributes.hvac_action === 'heating' ? 'var(--orange-color)':
              'none'
            } !important;
          }

          .bubble-icon {
            transform: scale(.9);
            color:  ${
              hass.states[entity].attributes.hvac_action != 'idle' ? 'var(--primary-background-color)': 'inherit'
            } !important;
            var(--primary-background-color) !important;
          }

          .bubble-range-fill {
            background: var(--accent-color) !important;
          }

          .bubble-name {
            font-weight: var(--ha-font-weight-medium); font-size: var(--ha-font-size-m);
          }

          .bubble-sub-button {
            # font-weight: bold;
            font-size: var(--ha-font-size-l);
            padding: 1px 9px 0 12px;
          }

          .bubble-temperature-container {
            display: none;
          }

          .bubble-sub-button-3 > ha-icon {
            color: ${hass.states['automation.climate_downstairs_comfort_control'].state == 'on' ? 
              'var(--orange-color)' : 'var(--dark-grey-color)' } !important; }
          ${subButtonIcon[0].setAttribute("icon",
          hass.states['automation.climate_downstairs_comfort_control'].state ===
          'on' ?
            'mdi:check-bold' : 'mdi:close-thick')}
        sub_button:
          - entity: climate.downstairs
            show_attribute: true
            attribute: current_temperature
            show_icon: false
            state_background: true
            show_background: false
            show_state: false
            show_last_changed: false
          - entity: climate.downstairs
            show_attribute: true
            attribute: current_humidity
            show_icon: false
            state_background: false
            show_background: false
          - entity: automation.climate_downstairs_comfort_control
            tap_action:
              action: toggle
            state_background: false
            icon: mdi:check-bold
  - type: markdown
    content: >

      {% set outside =
      states('sensor.weather_station_feels_like')|float %}

      {% set cold = is_state('input_boolean.cold_outside','on') %}

      {% set base_temp = states('input_number.house_base_temperature')|float %}

      {% set house_target =
      states('input_number.house_target_temperature')|float %}

      {% set presence = states('input_select.home_presence') %}


      ### House is {{ states('sensor.temperature_inside')|round(1) }}º ← [{{
      house_target|int }}°]
        {% if presence == 'Vacation' %}
          Vacation mode active ({{ '66' if cold else '78' }}º)
        {% else %}
          It's {{ outside|round(1) }}° and {{ 'cold' if cold else 'warm' }} outside
          Base ({{ base_temp|int }}º) + {{ "Offset (" }}
          {%- if cold -%}
            {%- if outside < 35 -%}+3
            {%- elif outside < 45 -%}+2
            {%- elif outside < 65 -%}+1
            {%- else -%}0
            {%- endif -%}
          {%- else -%}
            {%- if outside > 88 -%} -3
            {%- elif outside > 82 -%} -2
            {%- elif outside > 76 -%} -1
            {%- else -%} 0
            {%- endif -%}
          {%- endif -%}º)
        {%- endif -%}
        {{ " = "~house_target|int }}°

      {% set up_curr =
      state_attr('climate.upstairs','current_temperature')|float %}

      {% set up_low  = state_attr('climate.upstairs','target_temp_low')|float %}

      {% set up_high = state_attr('climate.upstairs','target_temp_high')|float
      %}

      {% set h = now().hour + now().minute/60 %}


      ### Upstairs is {{ up_curr|int }}° ← [{{ up_low|int }}–{{ up_high|int }}°]

      {% if presence == 'Vacation' %}
          (Vacation mode)
      {% else %}
        {% if 5.5 <= h < 7.5 %}
          05:30–07:30 — Morning preheat (+1°)
        {% elif 7.5 <= h < 20 %}
          07:30–20:00 — Baseline temp (0°)
        {% elif 20 <= h < 21 %}
          20:00–21:00 — Evening warm-up (+1°)
        {% else %}
          21:30–5:00 — Night setback (−1°)
        {% endif %}
      {% endif %}



      {% set off_curr = state_attr('climate.office','current_temperature')|float
      %}

      {% set off_target = state_attr('climate.office','temperature')|float %}

      {% set workday = is_state('binary_sensor.workday_sensor','on') %}


      ### Office is {{ off_curr|int }}° ← [{{ off_target|int }}°]

      {% if presence == 'Vacation' %}
          Vacation offset (−3°)
      {% else %}
        {%- if 22 <= now().hour or now().hour < 8 %}
          22:00–08:00 — Night setback (−2°)
        {%- elif workday and 8 <= now().hour < 9 %}
          08:00–09:00 — Workday preheat (+1°)
        {%- elif workday and 9 <= now().hour < 18 and is_state('person.alaa_shaker','home') %}
          09:00–18:00 — WFH baseline (+1°)
        {%- elif 8 <= now().hour < 22 %}
          08:00–18:00 — Baseline temp (0°)
        {%- else %}
          18:00—22:00 — Off-hours baseline (0°)
        {%- endif %}
      {%- endif %}

      {%- if off_curr > 72 %}
          Heating suppressed (already warm)
      {% endif %}



      {% set fr_curr =
      state_attr('climate.family_room','current_temperature')|float %}

      {% set fr_low  = state_attr('climate.family_room','target_temp_low')|float
      %}

      {% set fr_high =
      state_attr('climate.family_room','target_temp_high')|float %}


      ### Family Room is {{ fr_curr|int }}° ← [{{ fr_low|int }}–{{ fr_high|int
      }}°]

      {% if presence == 'Vacation' %}
          (Vacation mode)
      {% else %}
        {% if 6 <= now().hour < 10 %}
          06:00–10:00 – Elevated baseline (+1°)
        {% elif 10 <= now().hour < 23 %}
          10:00–23:00 — Baseline temp (0°)
        {% else %}
          23:00–06:00 — Night setback (−1°)
        {% endif %}
      {% endif %}



      {% set dn_curr =
      state_attr('climate.downstairs','current_temperature')|float %}

      {% set dn_target = state_attr('climate.downstairs','temperature')|float %}

      {% set dh = now().hour + now().minute/60 %}


      ### Downstairs is {{ dn_curr|int }}° ← [{{ dn_target|int }}°]

      {% if presence == 'Vacation' %}
          (Vacation mode)
      {% else %}
        {%- if 5.5 <= dh < 7.5 %}
          05:30–07:30 — Morning preheat (+1°)
        {%- elif 7.5 <= dh < 18 %}
          07:30–18:00 — Baseline temp (0°)
        {%- elif 18 <= dh < 19 %}
          18:00–19:00 — Evening warm-up (+1°)
        {%- else %}
          19:00–05:30 — Night baseline (0°)
        {%- endif %}
      {%- endif %}

      {%- if dn_curr > 72 %}
          Heating suppressed (already warm)
      {% endif %}

1 Like