Skip to content
Automate the North

A block heater that has the car warm right when you leave

A Home Assistant blueprint that starts a car block heater from your departure time and the outdoor temperature, and switches it off after you leave.

Published
Level
Intermediate
Hardware
Matter outdoor smart plug, outdoor temperature sensor

In a Finnish winter the car is plugged in every night. A plain timer on the outlet heats for the same time whether it’s −2 °C or −25 °C, so the car is either cold or the heater runs for nothing. This setup starts heating a set number of minutes before you leave, picks those minutes from the outdoor temperature, and switches the heater off after you’ve driven away.

You set two things per car: the departure time and whether scheduled heating is on. Everything else follows.

Home Assistant dashboard section for one car: a status line, block heater, scheduled heating, departure time and heating time.

What it does

  • Starts on time. Heating begins heating time minutes before departure. At +5 °C or warmer the heating time is zero and nothing runs.
  • Stops after you leave. The heater switches off 15 minutes after the departure time.
  • Handles manual starts. Switch the heater on by hand and it turns itself off after the heating time plus 15 minutes, so a forgotten heater never runs all day.
  • Reacts to changes you make. Move the departure time so heating should already be running and it starts straight away.
  • Doesn’t flip-flop with the weather. If it gets colder and the heating window now starts in the past, heating starts at once. Once heating has started, a warmer reading no longer cuts it short.
  • Survives restarts. If Home Assistant restarts during a heating window, heating resumes. A heater left running after a restart is switched off by a five-minute safety check.
  • Heating times are adjustable on the dashboard. One number per temperature band, no YAML editing.
  • Leaves the heater alone when the schedule is off. Then it’s a plain smart plug.

What you need

  • A smart plug rated for your block heater’s load and suitable for outdoor use. I use Matter outdoor plugs, but any plug that shows up as a switch in Home Assistant works.
  • An outdoor temperature sensor in Home Assistant.

Step 1: helpers and heating time

The package below creates the departure time, the schedule toggle, four heating times (one per temperature band), and two template sensors: how many minutes to heat right now, and when heating starts. Save it as config/packages/car_block_heater.yaml and replace sensor.outdoor_temperature with your own sensor.

Show the package
packages/car_block_heater.yaml
# Car block heater: helpers and heating time.
# Save as config/packages/car_block_heater.yaml (packages must be enabled in configuration.yaml).
# Replace sensor.outdoor_temperature with your own outdoor temperature sensor.
# For a second car, copy the block and change "car" to e.g. "car_2".
input_datetime:
car_departure_time:
name: Car departure time
has_date: false
has_time: true
icon: mdi:clock-outline
input_boolean:
car_heating_schedule:
name: Car scheduled heating
icon: mdi:calendar-clock
# Heating time for each temperature band. Adjust these from the dashboard;
# Home Assistant remembers the values across restarts.
input_number:
car_heating_minus_20:
name: Car heating time at −20 °C or colder
min: 0
max: 180
step: 5
mode: box
unit_of_measurement: min
car_heating_minus_10:
name: Car heating time at −10 to −20 °C
min: 0
max: 180
step: 5
mode: box
unit_of_measurement: min
car_heating_minus_5:
name: Car heating time at −5 to −10 °C
min: 0
max: 180
step: 5
mode: box
unit_of_measurement: min
car_heating_below_5:
name: Car heating time below +5 °C
min: 0
max: 180
step: 5
mode: box
unit_of_measurement: min
template:
- sensor:
# Minutes of heating at the current outdoor temperature, from the times above
- name: Car heating time
unique_id: car_heating_time
unit_of_measurement: min
icon: mdi:timer-outline
state: >
{% set t = states('sensor.outdoor_temperature') | float(99) %}
{% if t <= -20 %}{{ states('input_number.car_heating_minus_20') | int(0) }}
{% elif t <= -10 %}{{ states('input_number.car_heating_minus_10') | int(0) }}
{% elif t <= -5 %}{{ states('input_number.car_heating_minus_5') | int(0) }}
{% elif t < 5 %}{{ states('input_number.car_heating_below_5') | int(0) }}
{% else %}0
{% endif %}
# When heating starts, for the dashboard
- name: Car heating starts
unique_id: car_heating_starts
icon: mdi:clock-start
state: >
{% set h = state_attr('input_datetime.car_departure_time', 'hour') | int(0) %}
{% set m = state_attr('input_datetime.car_departure_time', 'minute') | int(0) %}
{% set dep = h * 60 + m %}
{% set dur = states('sensor.car_heating_time') | int(0) %}
{% if dur == 0 %}none{% else %}
{% set start = (dep - dur) % 1440 %}
{{ '%02d:%02d' | format(start // 60, start % 60) }}
{% endif %}

Where to set the heating times: on the dashboard from step 4, in the Heating times card. You can also find them under Settings → Devices & services → Entities by searching for “heating time”. New helpers start at 0 minutes, which means no heating, so enter your times once after setting this up.

These are the values I use for one car:

Outdoor temperature Heating time
−20 °C or colder 60 min
−10 °C … −20 °C 50 min
−5 °C … −10 °C 30 min
below +5 °C 20 min
+5 °C or warmer no heating

My second car gets 90, 60, 45 and 30 minutes. For two cars, copy the package block and rename car to car_2.

Packages need to be enabled once in configuration.yaml:

configuration.yaml
homeassistant:
packages: !include_dir_named packages

Then reload: Developer tools → YAML → Template entities, Input booleans, Input datetimes and Input numbers. A restart isn’t needed.

Step 2: the blueprint

The blueprint does the scheduling. Save it as config/blueprints/automation/automate_the_north/car_block_heater.yaml, then reload automations under Developer tools → YAML.

Show the blueprint
blueprints/automation/automate_the_north/car_block_heater.yaml
blueprint:
name: Car block heater by departure time
author: Automate the North
description: >
Switches a car engine block heater on so the car is warm at your departure time,
and off again shortly after you leave.
**How it works**
- Heating starts *heating time* minutes before the departure time. The heating
time comes from a sensor you choose (minutes), so you decide how long to heat
at each outdoor temperature. A heating time of 0 means no heating that day.
- The heater switches off a few minutes after departure.
- If you switch the heater on by hand, it switches itself off after the heating
time plus the after-departure buffer, so a forgotten heater never runs all day.
- If you change the departure time so heating should already be running, it
starts immediately.
- When the schedule toggle is off, this automation never touches the heater.
domain: automation
input:
heater:
name: Heater switch
description: The smart plug or relay that powers the block heater.
selector:
entity:
filter:
- domain: switch
departure_time:
name: Departure time
description: An input_datetime helper with time only.
selector:
entity:
filter:
- domain: input_datetime
schedule_enabled:
name: Scheduled heating toggle
description: An input_boolean that turns scheduled heating on or off.
selector:
entity:
filter:
- domain: input_boolean
heating_time:
name: Heating time sensor
description: A sensor whose state is the heating time in minutes, usually a template based on the outdoor temperature.
selector:
entity:
filter:
- domain: sensor
after_departure:
name: Keep heating after departure
description: Minutes the heater stays on after the departure time. Also the run time of a manual start on a warm day.
default: 15
selector:
number:
min: 0
max: 60
unit_of_measurement: min
mode: queued
max: 10
trigger_variables:
heater: !input heater
departure: !input departure_time
heating_time: !input heating_time
buffer: !input after_departure
triggers:
# Departure minus heating time
- trigger: template
id: start
value_template: >
{% set dep = (state_attr(departure, 'hour') | int(0)) * 60 + (state_attr(departure, 'minute') | int(0)) %}
{% set dur = states(heating_time) | int(0) %}
{{ dur > 0 and (now().hour * 60 + now().minute) == (dep - dur) % 1440 }}
# Departure plus buffer
- trigger: template
id: end
value_template: >
{% set dep = (state_attr(departure, 'hour') | int(0)) * 60 + (state_attr(departure, 'minute') | int(0)) %}
{{ (now().hour * 60 + now().minute) == (dep + buffer | int(0)) % 1440 }}
# Any run (manual or scheduled) ends after heating time + buffer at the latest
- trigger: state
id: timeout
entity_id: !input heater
to: 'on'
for:
minutes: '{{ [states(heating_time) | int(0), 0] | max + buffer | int(0) }}'
# Settings changed: start right away if we're already inside the heating window
- trigger: state
id: settings
entity_id:
- !input departure_time
- !input schedule_enabled
# Heating time grew so that the window now includes the current minute
- trigger: state
id: heating_time
entity_id: !input heating_time
# After a restart: resume a heating window that started while HA was down
- trigger: homeassistant
id: ha_start
event: start
# Safety net for runs whose timer was lost in a restart
- trigger: time_pattern
id: watchdog
minutes: /5
conditions:
- condition: state
entity_id: !input schedule_enabled
state: 'on'
variables:
now_min: '{{ now().hour * 60 + now().minute }}'
dep_min: "{{ (state_attr(departure, 'hour') | int(0)) * 60 + (state_attr(departure, 'minute') | int(0)) }}"
end_min: '{{ (dep_min + buffer | int(0)) % 1440 }}'
# Is now_min inside [departure - heating time, departure + buffer)? Wraps past midnight.
in_window: >-
{%- set dur = states(heating_time) | int(0) -%}
{%- set start = (dep_min - dur) % 1440 -%}
{{ dur > 0 and ((start <= end_min and start <= now_min and now_min < end_min)
or (start > end_min and (now_min >= start or now_min < end_min))) }}
# Same test with the heating time before this change (only for heating_time triggers)
was_in_window: >-
{%- set dur = trigger.from_state.state | int(0) if trigger.id == 'heating_time' else 0 -%}
{%- set start = (dep_min - dur) % 1440 -%}
{{ dur > 0 and ((start <= end_min and start <= now_min and now_min < end_min)
or (start > end_min and (now_min >= start or now_min < end_min))) }}
actions:
- choose:
- conditions:
- condition: trigger
id: start
- condition: template
value_template: "{{ is_state(heater, 'off') }}"
sequence:
- action: switch.turn_on
target:
entity_id: !input heater
- conditions:
- condition: trigger
id: [settings, heating_time, ha_start]
- condition: template
value_template: "{{ in_window | bool and not was_in_window | bool and is_state(heater, 'off') }}"
sequence:
- action: switch.turn_on
target:
entity_id: !input heater
- conditions:
- condition: trigger
id: [end, timeout]
- condition: template
value_template: "{{ is_state(heater, 'on') }}"
sequence:
- action: switch.turn_off
target:
entity_id: !input heater
# Heater left on outside the heating window for longer than a run may last
- conditions:
- condition: trigger
id: [watchdog, ha_start]
- condition: template
value_template: >-
{%- set on_min = (now() - states[heater].last_changed).total_seconds() / 60 -%}
{%- set limit = [states(heating_time) | int(0), 0] | max + buffer | int(0) -%}
{{ is_state(heater, 'on') and not in_window | bool and on_min >= limit }}
sequence:
- action: switch.turn_off
target:
entity_id: !input heater

Step 3: one automation per car

Go to Settings → Automations & scenes → Blueprints, pick Car block heater by departure time and fill in:

  • Heater switch: your smart plug
  • Departure time: input_datetime.car_departure_time
  • Scheduled heating toggle: input_boolean.car_heating_schedule
  • Heating time sensor: sensor.car_heating_time

Save. For a second car, create another automation from the same blueprint with that car’s entities.

Step 4: the dashboard

This section uses only built-in cards. The top line tells you in plain words what happens next, such as “Heating starts at 06:55”. The Heating times card at the bottom is where you adjust the minutes for each temperature band. In a Sections view, add a section, open Edit in YAML and paste:

Show the dashboard YAML
Dashboard section
# Dashboard section for one car. In a Sections view: add section → ⋮ → Edit in YAML, paste this.
type: 'grid'
cards:
- type: 'heading'
heading: 'Car'
heading_style: 'title'
icon: 'mdi:car'
badges:
- type: 'entity'
entity: 'sensor.outdoor_temperature'
name: 'Outside'
show_name: false
show_icon: true
icon: 'mdi:thermometer'
show_state: true
- type: 'markdown'
text_only: true
content: |-
{%- set t = states('sensor.outdoor_temperature') | float(none) -%}
{%- set dur = states('sensor.car_heating_time') | int(0) -%}
{%- set dep = states('input_datetime.car_departure_time')[:5] -%}
{%- if is_state('switch.car_block_heater','on') and is_state('input_boolean.car_heating_schedule','off') -%}
## Heater on
Scheduled heating is off, so the heater stays on until you switch it off.
{%- elif is_state('switch.car_block_heater','on') -%}
## Heating now
Ready for departure at {{ dep }}.
{%- elif is_state('input_boolean.car_heating_schedule','off') -%}
## Schedule off
The heater runs only when you switch it on.
{%- elif dur == 0 -%}
## No heating needed
It's {{ t | round(1) }} °C outside. The car is ready at {{ dep }} as it is.
{%- else -%}
## Heating starts at {{ states('sensor.car_heating_starts') }}
{{ dur }} minutes at {{ t | round(1) }} °C, warm by {{ dep }}.
{%- endif -%}
grid_options:
columns: 'full'
- type: 'tile'
entity: 'switch.car_block_heater'
name: 'Block heater'
icon: 'mdi:car-seat-heater'
vertical: false
grid_options:
columns: 6
tap_action:
action: 'toggle'
icon_tap_action:
action: 'toggle'
hide_state: true
- type: 'tile'
entity: 'input_boolean.car_heating_schedule'
name: 'Scheduled heating'
icon: 'mdi:calendar-clock'
vertical: false
grid_options:
columns: 6
tap_action:
action: 'toggle'
icon_tap_action:
action: 'toggle'
- type: 'tile'
entity: 'input_datetime.car_departure_time'
name: 'Departure'
icon: 'mdi:clock-outline'
vertical: false
grid_options:
columns: 6
- type: 'tile'
entity: 'sensor.car_heating_time'
name: 'Heating time'
icon: 'mdi:timer-outline'
vertical: false
grid_options:
columns: 6
- type: 'entities'
title: 'Heating times'
state_color: false
grid_options:
columns: 'full'
entities:
- entity: 'input_number.car_heating_minus_20'
name: '−20 °C or colder'
icon: 'mdi:thermometer'
- entity: 'input_number.car_heating_minus_10'
name: '−10 … −20 °C'
icon: 'mdi:thermometer'
- entity: 'input_number.car_heating_minus_5'
name: '−5 … −10 °C'
icon: 'mdi:thermometer'
- entity: 'input_number.car_heating_below_5'
name: 'below +5 °C'
icon: 'mdi:thermometer'

The block heater tile hides its state on purpose. The icon colour and the status line already say whether it’s on.

Good to know

  • Why the heating time is a sensor. The blueprint only needs a number of minutes. You can replace the temperature table with anything: wind chill, a different table for weekends, or a fixed value.
  • A manual start never outlasts the departure by more than 15 minutes while scheduled heating is on. A heater you switch on 10 minutes before departure stops 15 minutes after departure, not a full heating cycle later.