So, full transparancy, I don’t know how to code, the closest I can manage is to interpret code and be able to understand what it does(most of the time). So I have been using AI to help making my automations.
I just found this thread and I can see that AI has been in here snooping around and seen your project. Bacause I can see a lot of similarities.
So I asked the same AI to make a presentation of the project. And here it is. It has also included that I have wood stove and I use the balchony door to air out.
I have no idea of how good it is, but it seems to work. Maybe the coders in here can be the judge of this. I certanly can’t.
Prerequisites
You will need:
- A smart thermostat/heat pump integration (e.g., Sensibo, MelCloud, Z-Wave).
- Price data (e.g., Nordpool, Tibber) with future pricing attributes.
- Outdoor temperature sensor.
- Helpers (Input Booleans for
guest_mode, vacation_mode and Input Numbers for your target temperatures).
Part 1: The “Brain” (templates.yaml)
This section goes in your configuration.yaml or templates.yaml.
It creates two sensors:
- Heating Season: A binary sensor deciding if we need heat (based on Calendar OR Temp < 14°C).
- Heat Pump Manager: This calculates the strategy. It loops through future price data. It also calculates a dynamic
recommended_boost (e.g., if it is -10°C outside, we need to boost the temp more than if it is +5°C).
YAML
# ==============================================================================
# TEMPLATE SENSORS: CLIMATE LOGIC
# ==============================================================================
# 1. HEATING SEASON CALCULATOR
- binary_sensor:
- name: "Heating Season"
unique_id: heating_season_auto
icon: mdi:radiator
state: >
{# --- CONFIGURATION --- #}
{# Define winter months (e.g., Sept=9 to May=5) #}
{% set calendar_winter = now().month >= 9 or now().month <= 5 %}
{# Define cold threshold (e.g., below 14°C) #}
{# REPLACE: sensor.outdoor_temperature #}
{% set is_cold_outside = states('sensor.outdoor_temperature')|float(20) < 14 %}
{# Logic: On if calendar matches OR it is physically cold outside #}
{{ calendar_winter or is_cold_outside }}
# 2. HEAT PUMP MANAGER (THE BRAIN)
- sensor:
- name: "Heat Pump Manager"
unique_id: heat_pump_manager
icon: mdi:thermostat-auto
availability: >
{# REPLACE: These sensors with your actual price/data sensors #}
{{ states('sensor.electricity_price') | is_number and
state_attr('sensor.electricity_price_statistics', 'data') is not none and
states('sensor.outdoor_temperature') | is_number }}
state: >-
{# --- SETTINGS --- #}
{% set lookahead_hours = 3 %}
{% set cutoff_percent = 1.25 %} {# Cutoff if price is 25% above average #}
{% set cheap_percent = 0.90 %} {# Cheap if price is 90% of average #}
{# --- INPUTS (REPLACE THESE) --- #}
{% set current_price = states('sensor.electricity_price') | float(0) %}
{% set avg_price = state_attr('sensor.electricity_price_average', 'price_mean') | float(0) %}
{% set raw_data = state_attr('sensor.electricity_price_statistics', 'data') %}
{# --- CALCULATIONS --- #}
{% set limit_cutoff = avg_price * cutoff_percent %}
{% set limit_cheap = avg_price * cheap_percent %}
{# --- SPIKE DETECTION (Look ahead loop) --- #}
{% set ns = namespace(spike=false) %}
{% set now_ts = as_timestamp(now()) %}
{% set future_ts = now_ts + (lookahead_hours * 3600) %}
{% if raw_data is iterable %}
{% for item in raw_data %}
{# Handles different data formats (Nordpool/Tibber) #}
{% set t_start = item.start_time | default(item.start) %}
{% set t_price = item.price_per_kwh | default(item.price) %}
{% if t_start is defined %}
{% set ts = as_timestamp(t_start) %}
{# Logic: Is the future hour valid AND is price > cutoff? #}
{% if ts > now_ts and ts < future_ts and t_price > limit_cutoff %}
{% set ns.spike = true %}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{# --- STATE DETERMINATION --- #}
{% if current_price > limit_cutoff %} cutoff
{% elif current_price < limit_cutoff and ns.spike %} preheat
{% elif current_price < limit_cheap %} cheap
{% else %} normal
{% endif %}
attributes:
lookahead_hours: 3
# Helper attribute to visualize if a spike is detected
spike_detected: >
{% set raw_data = state_attr('sensor.electricity_price_statistics', 'data') %}
{% set avg = state_attr('sensor.electricity_price_average', 'price_mean') | float(0) %}
{% set limit = avg * 1.25 %}
{% set ns = namespace(spike=false) %}
{% set now_ts = as_timestamp(now()) %}
{% set future_ts = now_ts + (3 * 3600) %}
{% if raw_data is iterable %}
{% for item in raw_data %}
{% set t_start = item.start_time | default(item.start) %}
{% set t_price = item.price_per_kwh | default(item.price) %}
{% if t_start is defined %}
{% set ts = as_timestamp(t_start) %}
{% if ts > now_ts and ts < future_ts and t_price > limit %}
{% set ns.spike = true %}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{{ ns.spike }}
# Calculates how much to overheat based on outdoor temperature
recommended_boost: >
{% set temp = states('sensor.outdoor_temperature') | float(0) %}
{% set base_boost = 2.0 %}
{# Factor increases as it gets colder outside #}
{% set factor = 1.08 %}
{% if temp < -10 %} {% set factor = 1.45 %}
{% elif temp < -3 %} {% set factor = 1.34 %}
{% elif temp < 2 %} {% set factor = 1.24 %}
{% elif temp < 7 %} {% set factor = 1.14 %}
{% endif %}
{{ (base_boost * factor) | round(1) }}
Part 2: The Master Automation
This runs on any state change. It calculates the correct mode, temp, and fan speed, and only sends commands if something actually needs to change (to avoid API spamming).
Logic Flow:
- Summer: Keeps the house comfortable (Cool/Fan) but saves energy if no one is home.
- Winter:
- Vacation: Low temp.
- Preheat: Adds the calculated
boost_amount to the target temp.
- Away: Lowers temp if house is empty (Daytime).
- Normal/Sleep: Standard schedule.
YAML
alias: "Climate: Master Heat Pump Control"
description: "Smart control logic. Prioritizes 'Preheat' over 'Away' mode to maximize savings."
mode: restart
triggers:
- trigger: state
entity_id: binary_sensor.balcony_door
from: "on"
to: "off"
id: door_closes
- trigger: state
entity_id:
- sensor.heat_pump_manager # The Template Sensor from Part 1
- input_boolean.vacation_mode
- input_boolean.guest_mode
- zone.home # Presence detection
- binary_sensor.heating_season
- sensor.indoor_temperature
- trigger: time
at: ["05:00:00", "08:00:00", "16:00:00", "22:00:00"]
conditions:
- condition: state
entity_id: binary_sensor.balcony_door
state: "off"
actions:
- variables:
# --- GATHER SENSORS (REPLACE WITH YOUR ENTITY IDs) ---
current_temp: "{{ states('sensor.indoor_temperature') | float(20) }}"
is_winter: "{{ is_state('binary_sensor.heating_season', 'on') }}"
nobody_home: "{{ is_state('zone.home', '0') }}"
# --- MODES ---
is_vacation: "{{ is_state('input_boolean.vacation_mode', 'on') }}"
is_guest: "{{ is_state('input_boolean.guest_mode', 'on') }}"
manager_mode: "{{ states('sensor.heat_pump_manager') }}"
boost_amount: "{{ state_attr('sensor.heat_pump_manager', 'recommended_boost') | float(0) }}"
# --- USER SETPOINTS ---
setpoint_away: "{{ states('input_number.temp_away') | float(19) }}"
setpoint_normal: "{{ states('input_number.temp_normal') | float(22) }}"
setpoint_cheap: "{{ states('input_number.temp_cheap') | float(23) }}"
setpoint_sleep: 18
setpoint_vacation: 16
# --- LOGIC START ---
result: >-
{% set ns = namespace(mode='no_change', temp=none, fan='Auto') %}
{# === SUMMER LOGIC (COOLING) === #}
{% if not is_winter %}
{% if current_temp < 22.5 %}
{% set ns.mode = 'fan_only' %}
{% set ns.fan = 'Low' %}
{% elif is_vacation and not is_guest and current_temp > 27 %}
{% set ns.mode = 'cool' %}
{% set ns.temp = 27 %}
{% elif is_guest and current_temp > 24 %}
{% set ns.mode = 'cool' %}
{% set ns.temp = 23 %}
{% elif nobody_home and not is_vacation and not is_guest and current_temp > 25 %}
{% set ns.mode = 'cool' %}
{% set ns.temp = 25 %}
{% elif not nobody_home and current_temp > 24 %}
{% set ns.mode = 'cool' %}
{% set ns.temp = 23 %}
{% else %}
{% set ns.mode = 'off' %}
{% endif %}
{# === WINTER LOGIC (HEATING) === #}
{% else %}
{# 1. Overheat Protection #}
{% if current_temp > 25 %}
{% set ns.mode = 'fan_only' %}
{% set ns.fan = 'Medium' %}
{# 2. Hysteresis #}
{% elif current_temp > 23 and is_state('climate.heat_pump', 'fan_only') %}
{% set ns.mode = 'no_change' %}
{# 3. Standard Heating Priorities #}
{% else %}
{% set ns.mode = 'heat' %}
{% set target = setpoint_normal %}
{% set ns.fan = 'Auto' %}
{# A. Vacation #}
{% if is_vacation and not is_guest %}
{% set target = setpoint_vacation %}
{# B. Preheat (Smart Charging) - Overrides 'Away' #}
{% elif manager_mode == 'preheat' %}
{% set target = setpoint_normal + boost_amount %}
{% if 22 <= now().hour or now().hour < 6 %}
{% set ns.fan = 'Silence' %}
{% else %}
{% set ns.fan = 'High' %}
{% endif %}
{# C. Away (Daytime) #}
{% elif not is_guest and nobody_home and (8 <= now().hour < 16) %}
{% set target = setpoint_away %}
{# D. Sleep (Night) #}
{% elif 22 <= now().hour or now().hour < 5 %}
{% set target = setpoint_sleep %}
{% set ns.fan = 'Silence' %}
{# E. Normal Presence #}
{% else %}
{% if manager_mode == 'cutoff' %}
{% set target = 20 %}
{% set ns.fan = 'Silence' %}
{% elif manager_mode == 'cheap' %}
{% set target = setpoint_cheap %}
{% endif %}
{% endif %}
{% set ns.temp = target %}
{% endif %}
{% endif %}
{{ {'mode': ns.mode, 'temp': ns.temp, 'fan': ns.fan} | to_json }}
# --- APPLY SETTINGS ---
- variables:
settings: "{{ result | from_json }}"
- if:
- condition: template
value_template: "{{ settings.mode != 'no_change' }}"
then:
- if:
- condition: template
value_template: |-
{{ states('climate.heat_pump') != settings.mode or
(settings.mode in ['heat', 'cool'] and
(state_attr('climate.heat_pump', 'temperature') | float(0)) != (settings.temp | float(0))) }}
then:
- choose:
- conditions:
- condition: template
value_template: "{{ settings.mode in ['off', 'fan_only'] }}"
sequence:
- action: climate.set_hvac_mode
target:
entity_id: climate.heat_pump
data:
hvac_mode: "{{ settings.mode }}"
default:
- action: climate.set_temperature
target:
entity_id: climate.heat_pump
data:
hvac_mode: "{{ settings.mode }}"
temperature: "{{ settings.temp }}"
- delay: "00:00:02"
- if:
- condition: template
value_template: >-
{{ settings.mode != 'off' and
state_attr('climate.heat_pump', 'fan_mode') != settings.fan }}
then:
- action: climate.set_fan_mode
target:
entity_id: climate.heat_pump
data:
fan_mode: "{{ settings.fan }}"
Part 3: Door Safety Automation
A simple automation to prevent “firing for the crows” (heating the outdoors). If the balcony door opens, it kills the heat and turns off the air purifier.
YAML
alias: "Climate: Safety Cutoff (Door Open)"
description: "Turns off climate devices when the balcony door is opened."
mode: single
triggers:
- trigger: state
entity_id: binary_sensor.balcony_door
to: "on"
for: "00:00:10"
actions:
- action: switch.turn_on
target:
entity_id: switch.bathroom_fan_boost
- action: fan.turn_off
target:
entity_id: fan.air_purifier
- if:
- condition: not
conditions:
- condition: state
entity_id: climate.heat_pump
state: fan_only
then:
- action: climate.set_hvac_mode
target:
entity_id: climate.heat_pump
data:
hvac_mode: "off"