Skip to content

Instantly share code, notes, and snippets.

@Belphemur
Last active December 6, 2025 18:01
Show Gist options
  • Select an option

  • Save Belphemur/37384d2027c5b207caf827df488f6800 to your computer and use it in GitHub Desktop.

Select an option

Save Belphemur/37384d2027c5b207caf827df488f6800 to your computer and use it in GitHub Desktop.
Home Assistant: Automatically controls roof heating cables based on temperature and weather conditions.
blueprint:
name: "Smart Roof Heating - Restart-Proof Duty Cycle"
description: |
Redesigned heating cable controller with complete restart recovery.
Automatically detects and recovers duty cycle state after Home Assistant restart.
**Restart Recovery Features:**
• Detects if duty cycle was active before restart
• Automatically restarts timer with appropriate duration
• Maintains cable state consistency
• Handles all restart scenarios safely
**Architecture:**
• Weather triggers → Start duty cycle (if not running)
• Duty cycle helper → Prevents multiple duty cycle starts
• Only duty cycle → Controls cable ON/OFF switching
• Only duty cycle → Stops itself based on safety temperatures
• Startup recovery → Restores duty cycle after restart
• Forecast → Next hour only for activation decisions
domain: automation
author: "Home Assistant Community"
homeassistant:
min_version: "2025.11.0"
input:
weather_entity:
name: "Weather Entity"
description: "Weather entity that provides forecast data"
selector:
entity:
filter:
domain: weather
temperature_sensor:
name: "Temperature Sensor"
description: "Outdoor temperature sensor"
selector:
entity:
filter:
domain: sensor
device_class: temperature
precipitation_sensor:
name: "Precipitation Type Sensor"
description: "Sensor showing precipitation type (Snow, Rain, None, etc.)"
selector:
entity:
filter:
domain: sensor
snow_rate_sensor:
name: "Snow Rate Sensor"
description: "Sensor showing snow precipitation rate in mm/h"
selector:
entity:
filter:
domain: sensor
weather_condition_sensor:
name: "Weather Condition Sensor"
description: "Current weather condition sensor"
selector:
entity:
filter:
domain: sensor
forecast_storage_helper:
name: "Forecast Storage Helper"
description: "Input text helper to store next hour forecast"
selector:
entity:
filter:
domain: input_text
forecast_timestamp_helper:
name: "Forecast Timestamp Helper"
description: "Input datetime helper to track last forecast update"
selector:
entity:
filter:
domain: input_datetime
duty_cycle_active_helper:
name: "Duty Cycle Active Helper"
description: "Input boolean helper to track if duty cycle is running"
selector:
entity:
filter:
domain: input_boolean
duty_cycle_timer:
name: "Duty Cycle Timer Helper"
description: "Timer helper for duty cycling control"
selector:
entity:
filter:
domain: timer
heating_cable_switch:
name: "Heating Cable Switch"
description: "Switch entity that controls the roof heating cable"
selector:
entity:
filter:
domain: switch
notification_service:
name: "Mobile Notification Service"
description: "Mobile app notification service (e.g., mobile_app_pixel_8_pro)"
selector:
text:
duty_cycle_on_duration:
name: "Duty Cycle ON Duration"
description: "How long to keep cable ON during duty cycle"
default: 30
selector:
number:
min: 10
max: 120
unit_of_measurement: "minutes"
duty_cycle_off_duration:
name: "Duty Cycle OFF Duration"
description: "How long to keep cable OFF during duty cycle"
default: 30
selector:
number:
min: 10
max: 120
unit_of_measurement: "minutes"
day_temperature_threshold:
name: "Daytime Temperature Threshold"
description: "Temperature below which duty cycle starts during day"
default: 2
selector:
number:
min: -10
max: 10
unit_of_measurement: "°C"
night_temperature_threshold:
name: "Nighttime Temperature Threshold"
description: "Temperature below which duty cycle starts at night"
default: 4
selector:
number:
min: -10
max: 10
unit_of_measurement: "°C"
safety_shutoff_high_temp:
name: "Safety Shutoff Temperature (High)"
description: "Temperature above which duty cycle stops"
default: 10
selector:
number:
min: 5
max: 20
unit_of_measurement: "°C"
safety_shutoff_low_temp:
name: "Safety Shutoff Temperature (Low)"
description: "Temperature below which duty cycle stops"
default: -10
selector:
number:
min: -25
max: 0
unit_of_measurement: "°C"
variables:
weather_entity: !input weather_entity
temperature_sensor: !input temperature_sensor
precipitation_sensor: !input precipitation_sensor
snow_rate_sensor: !input snow_rate_sensor
weather_condition_sensor: !input weather_condition_sensor
forecast_storage: !input forecast_storage_helper
forecast_timestamp: !input forecast_timestamp_helper
duty_cycle_active: !input duty_cycle_active_helper
duty_timer: !input duty_cycle_timer
heating_switch: !input heating_cable_switch
notify_service: !input notification_service
duty_on_minutes: !input duty_cycle_on_duration
duty_off_minutes: !input duty_cycle_off_duration
day_temp: !input day_temperature_threshold
night_temp: !input night_temperature_threshold
safety_high_temp: !input safety_shutoff_high_temp
safety_low_temp: !input safety_shutoff_low_temp
trigger:
# ========== STARTUP RECOVERY TRIGGER ==========
- platform: homeassistant
event: start
id: "startup_recovery"
# Forecast data update (every 30 minutes)
- platform: time_pattern
minutes: "/30"
id: "fetch_forecast"
# Duty cycle timer finished
- platform: event
event_type: timer.finished
event_data:
entity_id: !input duty_cycle_timer
id: "duty_cycle_timer_finished"
# Weather condition triggers (for starting duty cycle)
- platform: state
entity_id: !input precipitation_sensor
to:
- "Snow"
- "Snow and Rain"
id: "weather_trigger"
- platform: numeric_state
entity_id: !input snow_rate_sensor
above: 0
id: "weather_trigger"
- platform: numeric_state
entity_id: !input temperature_sensor
below: !input day_temperature_threshold
id: "weather_trigger"
- platform: numeric_state
entity_id: !input temperature_sensor
below: !input night_temperature_threshold
id: "weather_trigger"
# Forecast data updates
- platform: state
entity_id: !input forecast_storage_helper
id: "weather_trigger"
condition: []
action:
# Update forecast data if needed
- if:
- condition: or
conditions:
- condition: trigger
id: "fetch_forecast"
- condition: trigger
id: "startup_recovery"
- condition: template
value_template: >
{{ (now() - states[forecast_timestamp].last_changed).total_seconds() > 1800 }}
then:
# Get next hour forecast only
- service: weather.get_forecasts
data:
type: hourly
target:
entity_id: "{{ weather_entity }}"
response_variable: forecast_response
# Store ONLY next hour forecast
- service: input_text.set_value
target:
entity_id: "{{ forecast_storage }}"
data:
value: >
{% if forecast_response and weather_entity in forecast_response %}
{% set forecast = forecast_response[weather_entity] %}
{% if forecast and 'forecast' in forecast and forecast.forecast|length > 0 %}
{% set next_hour = forecast.forecast[0] %}
{% if next_hour and 'condition' in next_hour %}
{{ next_hour.condition }}
{% else %}
no_data
{% endif %}
{% else %}
no_data
{% endif %}
{% else %}
no_data
{% endif %}
# Update timestamp
- service: input_datetime.set_datetime
target:
entity_id: "{{ forecast_timestamp }}"
data:
datetime: "{{ now().isoformat() }}"
# Main control logic
- choose:
# ========== STARTUP RECOVERY (HIGHEST PRIORITY) ==========
- conditions:
- condition: trigger
id: "startup_recovery"
sequence:
# Wait 30 seconds for all entities to be ready
- delay: "00:00:30"
# Check if duty cycle was active before restart
- if:
- condition: and
conditions:
# Duty cycle helper shows it was active
- condition: state
entity_id: !input duty_cycle_active_helper
state: "on"
# But timer is now idle (lost due to restart)
- condition: state
entity_id: !input duty_cycle_timer
state: "idle"
then:
# Check if conditions still require heating
- if:
- condition: and
conditions:
# Temperature is within safe operating range
- condition: numeric_state
entity_id: !input temperature_sensor
above: !input safety_shutoff_low_temp
- condition: numeric_state
entity_id: !input temperature_sensor
below: !input safety_shutoff_high_temp
# Temperature check (day/night aware)
- condition: or
conditions:
# Daytime threshold
- condition: and
conditions:
- condition: sun
after: sunrise
before: sunset
- condition: numeric_state
entity_id: !input temperature_sensor
below: !input day_temperature_threshold
# Nighttime threshold
- condition: and
conditions:
- condition: sun
after: sunset
before: sunrise
- condition: numeric_state
entity_id: !input temperature_sensor
below: !input night_temperature_threshold
# Weather conditions (current OR next hour)
- condition: or
conditions:
# Current snow conditions
- condition: state
entity_id: !input precipitation_sensor
state:
- "Snow"
- "Snow and Rain"
- condition: numeric_state
entity_id: !input snow_rate_sensor
above: 0
- condition: state
entity_id: !input weather_condition_sensor
state:
- "snowy"
- "snowy-rainy"
- "hail"
# Next hour forecast shows snow
- condition: template
value_template: >
{% set next_hour = states(forecast_storage) %}
{% if next_hour not in ['unavailable', 'unknown', 'no_data', ''] %}
{{ 'snow' in next_hour.lower() or 'sleet' in next_hour.lower() }}
{% else %}
false
{% endif %}
# Freezing rain
- condition: and
conditions:
- condition: numeric_state
entity_id: !input temperature_sensor
below: 0
- condition: state
entity_id: !input weather_condition_sensor
state:
- "rainy"
- "pouring"
then:
# RESTART DUTY CYCLE - Determine which phase to resume
- if:
- condition: state
entity_id: !input heating_cable_switch
state: "on"
then:
# Cable is ON, restart ON timer
- service: timer.start
target:
entity_id: "{{ duty_timer }}"
data:
duration: "00:{{ duty_on_minutes }}:00"
- service: persistent_notification.create
data:
notification_id: "roof_heating_status"
title: "🔄 Roof Heating - DUTY CYCLE RECOVERED"
message: |
**Status:** Duty cycle recovered after restart - Cable ON for {{ duty_on_minutes }} minutes
**Recovery Details:**
• Duty cycle was active before restart
• Timer restarted in ON phase
• Conditions still require heating
**Current Conditions:**
• Temperature: {{ states(temperature_sensor) }}°C
• Weather: {{ states(weather_condition_sensor) }}
• Precipitation: {{ states(precipitation_sensor) }}
*Duty cycle will continue normally*
- service: "notify.{{ notify_service }}"
data:
title: "🔄 Roof Heating - RECOVERED"
message: |
Duty cycle recovered after restart
Cable ON - timer restarted
Temperature: {{ states(temperature_sensor) }}°C
data:
tag: roof_heating
group: roof_heating
else:
# Cable is OFF, restart OFF timer
- service: timer.start
target:
entity_id: "{{ duty_timer }}"
data:
duration: "00:{{ duty_off_minutes }}:00"
- service: persistent_notification.create
data:
notification_id: "roof_heating_status"
title: "🔄 Roof Heating - DUTY CYCLE RECOVERED"
message: |
**Status:** Duty cycle recovered after restart - Cable OFF for {{ duty_off_minutes }} minutes
**Recovery Details:**
• Duty cycle was active before restart
• Timer restarted in OFF phase
• Conditions still require heating
**Current Conditions:**
• Temperature: {{ states(temperature_sensor) }}°C
• Weather: {{ states(weather_condition_sensor) }}
• Precipitation: {{ states(precipitation_sensor) }}
*Duty cycle will continue normally*
- service: "notify.{{ notify_service }}"
data:
title: "🔄 Roof Heating - RECOVERED"
message: |
Duty cycle recovered after restart
Cable OFF - timer restarted
Temperature: {{ states(temperature_sensor) }}°C
data:
tag: roof_heating
group: roof_heating
else:
# CONDITIONS NO LONGER REQUIRE HEATING - Stop duty cycle
- service: input_boolean.turn_off
target:
entity_id: "{{ duty_cycle_active }}"
- service: switch.turn_off
target:
entity_id: "{{ heating_switch }}"
- service: persistent_notification.create
data:
notification_id: "roof_heating_status"
title: "❄️ Roof Heating - STOPPED AFTER RESTART"
message: |
**Status:** Duty cycle stopped - conditions improved during restart
**Recovery Details:**
• Duty cycle was active before restart
• Conditions no longer require heating
• Cable turned off for safety
**Current Conditions:**
• Temperature: {{ states(temperature_sensor) }}°C
• Weather: {{ states(weather_condition_sensor) }}
• Precipitation: {{ states(precipitation_sensor) }}
*Duty cycle will restart if weather conditions require heating*
- service: "notify.{{ notify_service }}"
data:
title: "❄️ Roof Heating - STOPPED"
message: |
Duty cycle stopped after restart
Conditions improved during downtime
Temperature: {{ states(temperature_sensor) }}°C
data:
tag: roof_heating
group: roof_heating
# ========== DUTY CYCLE TIMER FINISHED ==========
- conditions:
- condition: trigger
id: "duty_cycle_timer_finished"
sequence:
# Check safety temperatures first
- if:
- condition: or
conditions:
# Temperature too high
- condition: numeric_state
entity_id: !input temperature_sensor
above: !input safety_shutoff_high_temp
# Temperature too low
- condition: numeric_state
entity_id: !input temperature_sensor
below: !input safety_shutoff_low_temp
then:
# STOP DUTY CYCLE - Safety temperature reached
- service: input_boolean.turn_off
target:
entity_id: "{{ duty_cycle_active }}"
- service: switch.turn_off
target:
entity_id: "{{ heating_switch }}"
- service: persistent_notification.create
data:
notification_id: "roof_heating_status"
title: "⚠️ Roof Heating - SAFETY STOP"
message: |
**Status:** Duty cycle STOPPED for safety
**Current Temperature:** {{ states(temperature_sensor) }}°C
**Safety Range:** {{ safety_low_temp }}°C to {{ safety_high_temp }}°C
**Reason:** {% if states(temperature_sensor)|float > safety_high_temp %}Temperature too high{% else %}Temperature too low{% endif %}
*Duty cycle will restart when conditions are safe and weather requires heating*
- service: "notify.{{ notify_service }}"
data:
title: "⚠️ Roof Heating - SAFETY STOP"
message: |
Duty cycle stopped for safety
Temperature: {{ states(temperature_sensor) }}°C
Reason: {% if states(temperature_sensor)|float > safety_high_temp %}Too hot (>{{ safety_high_temp }}°C){% else %}Too cold (<{{ safety_low_temp }}°C){% endif %}
data:
tag: roof_heating
group: roof_heating
priority: high
else:
# Check if conditions still require heating
- if:
- condition: and
conditions:
# Temperature check (day/night aware)
- condition: or
conditions:
# Daytime threshold
- condition: and
conditions:
- condition: sun
after: sunrise
before: sunset
- condition: numeric_state
entity_id: !input temperature_sensor
below: !input day_temperature_threshold
# Nighttime threshold
- condition: and
conditions:
- condition: sun
after: sunset
before: sunrise
- condition: numeric_state
entity_id: !input temperature_sensor
below: !input night_temperature_threshold
# Weather conditions (current OR next hour)
- condition: or
conditions:
# Current snow conditions
- condition: state
entity_id: !input precipitation_sensor
state:
- "Snow"
- "Snow and Rain"
- condition: numeric_state
entity_id: !input snow_rate_sensor
above: 0
- condition: state
entity_id: !input weather_condition_sensor
state:
- "snowy"
- "snowy-rainy"
- "hail"
# Next hour forecast shows snow
- condition: template
value_template: >
{% set next_hour = states(forecast_storage) %}
{% if next_hour not in ['unavailable', 'unknown', 'no_data', ''] %}
{{ 'snow' in next_hour.lower() or 'sleet' in next_hour.lower() }}
{% else %}
false
{% endif %}
# Freezing rain
- condition: and
conditions:
- condition: numeric_state
entity_id: !input temperature_sensor
below: 0
- condition: state
entity_id: !input weather_condition_sensor
state:
- "rainy"
- "pouring"
then:
# CONTINUE DUTY CYCLE - Toggle cable state
- if:
- condition: state
entity_id: !input heating_cable_switch
state: "on"
then:
# Cable is ON, turn OFF and start OFF timer
- service: switch.turn_off
target:
entity_id: "{{ heating_switch }}"
- service: timer.start
target:
entity_id: "{{ duty_timer }}"
data:
duration: "00:{{ duty_off_minutes }}:00"
- service: persistent_notification.create
data:
notification_id: "roof_heating_status"
title: "🔄 Roof Heating - DUTY CYCLE OFF"
message: |
**Status:** Cable OFF for {{ duty_off_minutes }} minutes
**Current Conditions:**
• Temperature: {{ states(temperature_sensor) }}°C
• Weather: {{ states(weather_condition_sensor) }}
• Precipitation: {{ states(precipitation_sensor) }}
• Next Hour: {{ states(forecast_storage) }}
**Energy Saving:** Duty cycling active
*Cable will turn ON automatically in {{ duty_off_minutes }} minutes*
else:
# Cable is OFF, turn ON and start ON timer
- service: switch.turn_on
target:
entity_id: "{{ heating_switch }}"
- service: timer.start
target:
entity_id: "{{ duty_timer }}"
data:
duration: "00:{{ duty_on_minutes }}:00"
- service: persistent_notification.create
data:
notification_id: "roof_heating_status"
title: "🔥 Roof Heating - DUTY CYCLE ON"
message: |
**Status:** Cable ON for {{ duty_on_minutes }} minutes
**Current Conditions:**
• Temperature: {{ states(temperature_sensor) }}°C
• Weather: {{ states(weather_condition_sensor) }}
• Precipitation: {{ states(precipitation_sensor) }}
• Next Hour: {{ states(forecast_storage) }}
**Energy Saving:** Duty cycling active
*Cable will turn OFF automatically in {{ duty_on_minutes }} minutes*
else:
# STOP DUTY CYCLE - Conditions no longer require heating
- service: input_boolean.turn_off
target:
entity_id: "{{ duty_cycle_active }}"
- service: switch.turn_off
target:
entity_id: "{{ heating_switch }}"
- service: persistent_notification.create
data:
notification_id: "roof_heating_status"
title: "❄️ Roof Heating - INACTIVE"
message: |
**Status:** Duty cycle stopped - conditions improved
**Current Conditions:**
• Temperature: {{ states(temperature_sensor) }}°C
• Weather: {{ states(weather_condition_sensor) }}
• Precipitation: {{ states(precipitation_sensor) }}
• Next Hour: {{ states(forecast_storage) }}
*Duty cycle will restart if weather conditions require heating*
- service: "notify.{{ notify_service }}"
data:
title: "❄️ Roof Heating Deactivated"
message: |
Duty cycle stopped - conditions improved
Temperature: {{ states(temperature_sensor) }}°C
Weather: {{ states(weather_condition_sensor) }}
data:
tag: roof_heating
group: roof_heating
# ========== WEATHER TRIGGERS (START DUTY CYCLE) ==========
- conditions:
- condition: and
conditions:
# Duty cycle is NOT currently active
- condition: state
entity_id: !input duty_cycle_active_helper
state: "off"
# Temperature is within safe operating range
- condition: numeric_state
entity_id: !input temperature_sensor
above: !input safety_shutoff_low_temp
- condition: numeric_state
entity_id: !input temperature_sensor
below: !input safety_shutoff_high_temp
# Temperature check (day/night aware)
- condition: or
conditions:
# Daytime threshold
- condition: and
conditions:
- condition: sun
after: sunrise
before: sunset
- condition: numeric_state
entity_id: !input temperature_sensor
below: !input day_temperature_threshold
# Nighttime threshold
- condition: and
conditions:
- condition: sun
after: sunset
before: sunrise
- condition: numeric_state
entity_id: !input temperature_sensor
below: !input night_temperature_threshold
# Weather conditions (current OR next hour)
- condition: or
conditions:
# Current snow conditions
- condition: state
entity_id: !input precipitation_sensor
state:
- "Snow"
- "Snow and Rain"
- condition: numeric_state
entity_id: !input snow_rate_sensor
above: 0
- condition: state
entity_id: !input weather_condition_sensor
state:
- "snowy"
- "snowy-rainy"
- "hail"
# Next hour forecast shows snow
- condition: template
value_template: >
{% set next_hour = states(forecast_storage) %}
{% if next_hour not in ['unavailable', 'unknown', 'no_data', ''] %}
{{ 'snow' in next_hour.lower() or 'sleet' in next_hour.lower() }}
{% else %}
false
{% endif %}
# Freezing rain
- condition: and
conditions:
- condition: numeric_state
entity_id: !input temperature_sensor
below: 0
- condition: state
entity_id: !input weather_condition_sensor
state:
- "rainy"
- "pouring"
sequence:
# START DUTY CYCLE
- service: input_boolean.turn_on
target:
entity_id: "{{ duty_cycle_active }}"
# Turn on cable and start first ON timer
- service: switch.turn_on
target:
entity_id: "{{ heating_switch }}"
- service: timer.start
target:
entity_id: "{{ duty_timer }}"
data:
duration: "00:{{ duty_on_minutes }}:00"
# Notifications
- service: persistent_notification.create
data:
notification_id: "roof_heating_status"
title: "🔥 Roof Heating - DUTY CYCLE STARTED"
message: |
**Status:** Duty cycle started - Cable ON for {{ duty_on_minutes }} minutes
**Current Conditions:**
• Temperature: {{ states(temperature_sensor) }}°C
• Weather: {{ states(weather_condition_sensor) }}
• Precipitation: {{ states(precipitation_sensor) }}
• Next Hour: {{ states(forecast_storage) }}
**Duty Cycle:** {{ duty_on_minutes }}min ON / {{ duty_off_minutes }}min OFF
**Safety Range:** {{ safety_low_temp }}°C to {{ safety_high_temp }}°C
*Only duty cycle controls cable from now on*
- service: "notify.{{ notify_service }}"
data:
title: "🔥 Roof Heating - DUTY CYCLE STARTED"
message: |
Duty cycle started - {{ duty_on_minutes }}min ON / {{ duty_off_minutes }}min OFF
Temperature: {{ states(temperature_sensor) }}°C
Weather: {{ states(weather_condition_sensor) }}
Next Hour: {{ states(forecast_storage) }}
data:
tag: roof_heating
group: roof_heating
priority: high
mode: queued
max: 5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment