Car preheat automation (defa/calix/etc.)

Hi,

I’m planning to automate my car heating setup and would appreciate some advice on the best approach.

Current situation

  • I have 2* standard 230V outlet for engine/block heating.
  • Control is currently manual or based on simple timers.

What I want to achieve

  • Set a departure time (e.g. 07:30), and have the system automatically determine when to start heating.
  • Heating duration should adapt based on outdoor temperature:
    • colder weather → longer heating
    • milder weather → shorter heating
  • Heating should continue for 30 minutes after the departure time.
  • If the departure time has passed and no power is being consumed, the system should turn off automatically.
  • I want it to work like smartphone alarmclock, if you select days it will repeat on those days and if you don’t then it will heat just next day.

Manual control requirement

  • Each outlet (shelly 1PM gen4) has a physical button (via Shelly input) to turn it on manually.
  • When activated manually:
    • the outlet should turn on immediately (it does because of shelly)
    • and then automatically turn off after a configurable time (set via a variable/helper)

Data sources I’m considering

  • Weather forecast (online API)
  • Local sensors:
    • 2*Hue outdoor motion sensors)

Goal
A reliable, mostly automated system where I only need to:

  • set the departure time
  • and everything else happens automatically

What have I achieved already

  • Script that calculates heating start time and how logn to heat. Is this logic DST proof?:
car_heating_time_calculator:
  alias: Car Heating Time Calculator
  mode: single
  sequence:

    # ===== Car 1, Profile 1 =====
    - service: input_number.set_value
      target:
        entity_id: input_number.car1_heat_duration_1
      data:
        value: >
            {% set outdoor = states('sensor.outdoor_temperature_min') | float(999) %}
            {% set e = 2.718281828459045 %}
            {% set val = 0.5 * (e ** (0.075 * (4 - outdoor))) %}
            {{ [4, [0.5, val] | max] | min | round(2) }}

    - service: input_datetime.set_datetime
      target:
        entity_id: input_datetime.car1_heat_start_1
      data:
        time: >
            {% set dep_time = states('input_datetime.autostart_1_1')[0:5] %}
            {% set dep_today = today_at(dep_time) %}
            {% set dep_dt = dep_today if dep_today > now() else dep_today + timedelta(days=1) %}
            {% set dep_ts = as_timestamp(dep_dt) %}
            {% set heat_h = states('input_number.car1_heat_duration_1') | float(4) %}
            {{ (dep_ts - (heat_h * 3600)) | timestamp_custom('%H:%M', true) }}

    # ===== Car 1, Profile 2 =====
    - service: input_number.set_value
      target:
        entity_id: input_number.car1_heat_duration_2
      data:
        value: >
            {% set outdoor = states('sensor.outdoor_temperature_min') | float(999) %}
            {% set e = 2.718281828459045 %}
            {% set val = 0.5 * (e ** (0.075 * (4 - outdoor))) %}
            {{ [4, [0.5, val] | max] | min | round(2) }}
          
    - service: input_datetime.set_datetime
      target:
        entity_id: input_datetime.car1_heat_start_2
      data:
        time: >
            {% set dep_time = states('input_datetime.autostart_1_2')[0:5] %}
            {% set dep_today = today_at(dep_time) %}
            {% set dep_dt = dep_today if dep_today > now() else dep_today + timedelta(days=1) %}
            {% set dep_ts = as_timestamp(dep_dt) %}
            {% set heat_h = states('input_number.car1_heat_duration_1') | float(4) %}
            {{ (dep_ts - (heat_h * 3600)) | timestamp_custom('%H:%M', true) }}

    # ===== Car 1, Profile 3 =====
    - service: input_number.set_value
      target:
        entity_id: input_number.car1_heat_duration_3
      data:
        value: >
            {% set outdoor = states('sensor.outdoor_temperature_min') | float(999) %}
            {% set e = 2.718281828459045 %}
            {% set val = 0.5 * (e ** (0.075 * (4 - outdoor))) %}
            {{ [4, [0.5, val] | max] | min | round(2) }}

    - service: input_datetime.set_datetime
      target:
        entity_id: input_datetime.car1_heat_start_3
      data:
        time: >
            {% set dep_time = states('input_datetime.autostart_1_3')[0:5] %}
            {% set dep_today = today_at(dep_time) %}
            {% set dep_dt = dep_today if dep_today > now() else dep_today + timedelta(days=1) %}
            {% set dep_ts = as_timestamp(dep_dt) %}
            {% set heat_h = states('input_number.car1_heat_duration_1') | float(4) %}
            {{ (dep_ts - (heat_h * 3600)) | timestamp_custom('%H:%M', true) }}

    # ===== Car 2, Profile 1 =====
    - service: input_number.set_value
      target:
        entity_id: input_number.car2_heat_duration_1
      data:
        value: >
            {% set outdoor = states('sensor.outdoor_temperature_min') | float(999) %}
            {% set e = 2.718281828459045 %}
            {% set val = 0.5 * (e ** (0.075 * (4 - outdoor))) %}
            {{ [4, [0.5, val] | max] | min | round(2) }}

    - service: input_datetime.set_datetime
      target:
        entity_id: input_datetime.car2_heat_start_1
      data:
        time: >
            {% set dep_time = states('input_datetime.autostart_2_1')[0:5] %}
            {% set dep_today = today_at(dep_time) %}
            {% set dep_dt = dep_today if dep_today > now() else dep_today + timedelta(days=1) %}
            {% set dep_ts = as_timestamp(dep_dt) %}
            {% set heat_h = states('input_number.car1_heat_duration_1') | float(4) %}
            {{ (dep_ts - (heat_h * 3600)) | timestamp_custom('%H:%M', true) }}

    # ===== Car 2, Profile 2 =====
    - service: input_number.set_value
      target:
        entity_id: input_number.car2_heat_duration_2
      data:
        value: >
            {% set outdoor = states('sensor.outdoor_temperature_min') | float(999) %}
            {% set e = 2.718281828459045 %}
            {% set val = 0.5 * (e ** (0.075 * (4 - outdoor))) %}
            {{ [4, [0.5, val] | max] | min | round(2) }}

    - service: input_datetime.set_datetime
      target:
        entity_id: input_datetime.car2_heat_start_2
      data:
        time: >
            {% set dep_time = states('input_datetime.autostart_2_2')[0:5] %}
            {% set dep_today = today_at(dep_time) %}
            {% set dep_dt = dep_today if dep_today > now() else dep_today + timedelta(days=1) %}
            {% set dep_ts = as_timestamp(dep_dt) %}
            {% set heat_h = states('input_number.car1_heat_duration_1') | float(4) %}
            {{ (dep_ts - (heat_h * 3600)) | timestamp_custom('%H:%M', true) }}

    # ===== Car 2, Profile 3 =====
    - service: input_number.set_value
      target:
        entity_id: input_number.car2_heat_duration_3
      data:
        value: >
            {% set outdoor = states('sensor.outdoor_temperature_min') | float(999) %}
            {% set e = 2.718281828459045 %}
            {% set val = 0.5 * (e ** (0.075 * (4 - outdoor))) %}
            {{ [4, [0.5, val] | max] | min | round(2) }}

    - service: input_datetime.set_datetime
      target:
        entity_id: input_datetime.car2_heat_start_3
      data:
        time: >
            {% set dep_time = states('input_datetime.autostart_2_3')[0:5] %}
            {% set dep_today = today_at(dep_time) %}
            {% set dep_dt = dep_today if dep_today > now() else dep_today + timedelta(days=1) %}
            {% set dep_ts = as_timestamp(dep_dt) %}
            {% set heat_h = states('input_number.car1_heat_duration_1') | float(4) %}
            {{ (dep_ts - (heat_h * 3600)) | timestamp_custom('%H:%M', true) }}

I also have UI cards:

type: vertical-stack
title: Lämmityspistorasia A
cards:
  - type: entities
    title: Lämpötila
    entities:
      - entity: sensor.outdoor_temperature_min
        name: Kylmin ulkolämpötila
        icon: mdi:snowflake
      - entity: input_button.update_car_heating_calc
        name: Päivitä lämmityslaskenta
        icon: mdi:refresh
  - type: entities
    title: Manuaalinen lämmitys
    entities:
      - entity: switch.autolammitys_a
        name: Lämmitys päälle/pois
      - entity: input_number.manual_heat_duration_1
        name: Lämmityksen kesto (h)
      - entity: sensor.shelly1pmg4_xxx_current
        name: Virta (A)
  - type: entities
    title: Profiili 1
    entities:
      - entity: input_boolean.autostart_1_1_enabled
        name: Profiili käytössä
      - entity: input_datetime.autostart_1_1
        name: Lähtöaika
      - entity: sensor.car_1_heat_start_1
        name: Laskettu aloitusaika
      - type: section
        label: Toistuvat päivät
      - entity: input_boolean.autostart_1_1_mon
        name: Maanantai
      - entity: input_boolean.autostart_1_1_tue
        name: Tiistai
      - entity: input_boolean.autostart_1_1_wed
        name: Keskiviikko
      - entity: input_boolean.autostart_1_1_thu
        name: Torstai
      - entity: input_boolean.autostart_1_1_fri
        name: Perjantai
      - entity: input_boolean.autostart_1_1_sat
        name: Lauantai
      - entity: input_boolean.autostart_1_1_sun
        name: Sunnuntai
  - type: entities
    title: Profiili 2
    entities:
      - entity: input_boolean.autostart_1_2_enabled
        name: Profiili käytössä
      - entity: input_datetime.autostart_1_2
        name: Lähtöaika
      - entity: sensor.car_1_heat_start_2
        name: Laskettu aloitusaika
      - type: section
        label: Toistuvat päivät
      - entity: input_boolean.autostart_1_2_mon
        name: Maanantai
      - entity: input_boolean.autostart_1_2_tue
        name: Tiistai
      - entity: input_boolean.autostart_1_2_wed
        name: Keskiviikko
      - entity: input_boolean.autostart_1_2_thu
        name: Torstai
      - entity: input_boolean.autostart_1_2_fri
        name: Perjantai
      - entity: input_boolean.autostart_1_2_sat
        name: Lauantai
      - entity: input_boolean.autostart_1_2_sun
        name: Sunnuntai
  - type: entities
    title: Profiili 3
    entities:
      - entity: input_boolean.autostart_1_3_enabled
        name: Profiili käytössä
      - entity: input_datetime.autostart_1_3
        name: Lähtöaika
      - entity: sensor.car_1_heat_start_3
        name: Laskettu aloitusaika
      - type: section
        label: Toistuvat päivät
      - entity: input_boolean.autostart_1_3_mon
        name: Maanantai
      - entity: input_boolean.autostart_1_3_tue
        name: Tiistai
      - entity: input_boolean.autostart_1_3_wed
        name: Keskiviikko
      - entity: input_boolean.autostart_1_3_thu
        name: Torstai
      - entity: input_boolean.autostart_1_3_fri
        name: Perjantai
      - entity: input_boolean.autostart_1_3_sat
        name: Lauantai
      - entity: input_boolean.autostart_1_3_sun
        name: Sunnuntai

I’m really new to HA and this is my first real automation :smiley:
Thanks in advance for any suggestions or examples!

Start simple:

  • Write a very simple automation.
  • Use it for a bit - to see if it works / there is something you haven’t thought of.
  • Fix any issues.
  • Again use/test it for a bit.
  • Then find another small feature to add and repeat the previous steps.

Start by using HA to implement a dumb timer:

  • Turn on at 6:30 AM
  • Turn off at 8:00 AM

Then add new features over time:

  • Implement your manual override requirements.
  • Make the timer smarter:
    • Skip some days.
    • Different departure times on different days.
  • Implement your temperature based optimization **

** - Running the heater for less time on warmer days is really just a cost saving - it won’t materially affect how you use it - that’s why I would leave it until last.


Before you do temperature based optimization, I would look into what hardware you need:

  • I don’t like using weather forecasts - they don’t match the actual temperature very closely.
  • Secondary functions (such as temperature on a PIR sensor) don’t always provide updates, unless the primary function (PIR) is triggered.
  • Battery devices don’t provide as frequent updates as wired devices

TL;DR - You might want to buy a proper temperature sensor - doesn’t have to be super expensive - just needs to be an actual temperature sensor.

Thanks for the feedback, I appreciate the perspective.

That said, if my goal were just to run a dumb timer (“on at 6:30, off at 8”), I’d honestly just install a simple analog clock timer and be done with it :smile:

The main reason I’m using Home Assistant is precisely to move beyond that and make the system smarter and more automated. In fact, this car heating project was the primary reason I started using HA in the first place.

I do agree with your point about sensor reliability. My thinking is that it’s relatively easy to add proper, dedicated temperature sensors later, once the core automation logic (start/stop, overrides, scheduling) is proven to work reliably. For now, I’m combining the temperatures from two Hue outdoor motion sensors with forecast data and using the lowest value.

I’d like to get the overall flow right first, and then refine the inputs and optimization on top of that.

The incremental approach you suggest is definitely something I’ll keep in mind as I move forward.

I’ve made some good progress on this.

I ended up creating a separate script for each car/profile. Each script is responsible for calculating:

  • the next departure time
  • the heating start time
  • the heating duration
car_heating_x_y:
  alias: CarxPy Heating
  mode: single
  sequence:

    # =========================================================
    # VARIABLES
    # =========================================================
    - variables:

        dep_time: "{{ states('input_datetime.autostart_x_y')[0:5] }}"

        enabled: "{{ is_state('input_boolean.autostart_x_y_enabled', 'on') }}"

        duration_entity: "input_number.carx_heat_duration_y"
        start_entity: "input_datetime.carx_heat_start_y"
        dep_entity: "input_datetime.carx_dep_dt_y"
        heating_sensor: "binary_sensor.carx_heating_auto_y"

        sensor_temp: "sensor.outdoor_temperature_min"

        weekdays:
          - "{{ states('input_boolean.autostart_x_y_mon') }}"
          - "{{ states('input_boolean.autostart_x_y_tue') }}"
          - "{{ states('input_boolean.autostart_x_y_wed') }}"
          - "{{ states('input_boolean.autostart_x_y_thu') }}"
          - "{{ states('input_boolean.autostart_x_y_fri') }}"
          - "{{ states('input_boolean.autostart_x_y_sat') }}"
          - "{{ states('input_boolean.autostart_x_y_sun') }}"

        now_ts: "{{ as_timestamp(now()) }}"

    # =========================================================
    # FIND NEXT VALID DEPARTURE
    # =========================================================
    - variables:
        dep_ts: >
          {% set time = dep_time %}
          {% set days = weekdays %}
          {% set any_days = days | select('eq','on') | list | length > 0 %}
          {% set ns = namespace(found=none) %}

          {% for offset in range(0, 14) %}
            {% set candidate = today_at(time) + timedelta(days=offset) %}
            {% set ts = as_timestamp(candidate) %}

            {% if ts > now_ts %}
              {% set dow = candidate.weekday() %}
              {% if not any_days or days[dow] == 'on' %}
                {% set ns.found = ts %}
                {% break %}
              {% endif %}
            {% endif %}
          {% endfor %}

          {{ ns.found }}
          
    # =========================================================
    # DISABLE LOGIC (FORCE PAST START TIME)
    # =========================================================
    - if:
        - condition: template
          value_template: >
            {% set start = states(start_entity) %}
            {{ not enabled
               and states(heating_sensor) == 'off'
               and start not in ['unknown','unavailable','']
               and as_timestamp(start | as_datetime | as_local) > now_ts }}
      then:
        - service: input_datetime.set_datetime
          target:
            entity_id: "{{ start_entity }}"
          data:
            datetime: >
              {{ (now() - timedelta(days=2)).strftime('%Y-%m-%d %H:%M:%S') }}

        - service: input_datetime.set_datetime
          target:
            entity_id: "{{ dep_entity }}"
          data:
            datetime: >
              {{ (now() - timedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S') }}
              
    # =========================================================
    # GUARD (PROFILE ACTIVE)
    # =========================================================
    - if:
        - condition: template
          value_template: >
            {{ enabled and dep_ts is not none }}
      then:

        # =====================================================
        # HEAT DURATION
        # =====================================================
        - variables:
            heat_h: >
              {% set outdoor = states(sensor_temp) | float(999) %}
              {% set e = 2.718281828459045 %}
              {% set val = 0.5 * (e ** (0.075 * (4 - outdoor))) %}
              {{ [4, [0.5, val] | max] | min | round(2) }}

        # =====================================================
        # SAVE DURATION
        # =====================================================
        - service: input_number.set_value
          target:
            entity_id: "{{ duration_entity }}"
          data:
            value: "{{ heat_h }}"

        # =====================================================
        # START TIME
        # =====================================================
        - service: input_datetime.set_datetime
          target:
            entity_id: "{{ start_entity }}"
          data:
            datetime: >
              {{ (dep_ts - (heat_h * 3600))
                 | timestamp_custom('%Y-%m-%d %H:%M:%S', true) }}

        # =====================================================
        # DEPARTURE TIME
        # =====================================================
        - service: input_datetime.set_datetime
          target:
            entity_id: "{{ dep_entity }}"
          data:
            datetime: >
              {{ dep_ts
                 | timestamp_custom('%Y-%m-%d %H:%M:%S', true) }}

These scripts are then called by different automations depending on the situation.

For temperature input, I expanded the logic a bit. The heating duration is now based on the lowest value from:

  • Hue outdoor motion sensor 1
  • Hue outdoor motion sensor 2
  • current temperature from the weather service
  • minimum temperature from the next 5 hours (hourly forecast)

The lowest of these is used to determine the heating duration.

I also created a separate script for manual heating, which calculates the shutdown time and determines whether the system is currently in manual or automatic heating mode.

carx_heating_mode_and_manual_shutdown:
  alias: Carx Heating Mode + Shutdown Calculator
  mode: single
  sequence:

    # =========================================================
    # INPUTS
    # =========================================================
    - variables:
        auto_sensor_1: "binary_sensor.carx_heating_auto_1"
        auto_sensor_2: "binary_sensor.carx_heating_auto_2"
        auto_sensor_3: "binary_sensor.carx_heating_auto_3"

        master_switch: "switch.autolammitys_a"

        duration_entity: "input_number.manual_heat_duration_x"
        auto_mode_entity: "input_boolean.carx_heating_auto"
        manual_mode_entity: "input_boolean.carx_heating_manual"
        shutdown_entity: "input_datetime.carx_manual_shutdown"

    # =========================================================
    # MANUAL SHUTDOWN CALCULATION
    # =========================================================
    - variables:
        heat_h: "{{ states(duration_entity) | float(0) }}"

        shutdown_dt: >
          {{ (now() + timedelta(hours=heat_h))
             .strftime('%Y-%m-%d %H:%M:%S') }}

    - service: input_datetime.set_datetime
      target:
        entity_id: "{{ shutdown_entity }}"
      data:
        datetime: "{{ shutdown_dt }}"
    # =========================================================
    # MASTER SWITCH CHECK
    # =========================================================
    - if:
        - condition: template
          value_template: >
            {{ not is_state(master_switch, 'on') }}

      then:
        # -----------------------------------------------------
        # POWER OFF
        # -----------------------------------------------------
        - service: input_boolean.turn_off
          target:
            entity_id: "{{ auto_mode_entity }}"

        - service: input_boolean.turn_off
          target:
            entity_id: "{{ manual_mode_entity }}"

        - stop: "Switch OFF → no calculation"

    # =========================================================
    # AUTO DETECTION
    # =========================================================
    - variables:
        is_auto: >
          {{ is_state(auto_sensor_1, 'on')
             or is_state(auto_sensor_2, 'on')
             or is_state(auto_sensor_3, 'on') }}

    # =========================================================
    # MODE SET
    # =========================================================
    - choose:
        - conditions:
            - condition: template
              value_template: "{{ is_auto }}"
          sequence:
            - service: input_boolean.turn_on
              target:
                entity_id: "{{ auto_mode_entity }}"

            - service: input_boolean.turn_off
              target:
                entity_id: "{{ manual_mode_entity }}"

      default:
        - service: input_boolean.turn_off
          target:
            entity_id: "{{ auto_mode_entity }}"

        - service: input_boolean.turn_on
          target:
            entity_id: "{{ manual_mode_entity }}"

Automations:

- id: '1776100945869'
  alias: Autolammitys A Auto
  description: ''
  triggers:
  - trigger: state
    entity_id:
    - binary_sensor.car1_heating_auto_1
    - binary_sensor.car1_heating_auto_2
    - binary_sensor.car1_heating_auto_3
    to:
  - trigger: time_pattern
    minutes: /1
  conditions:
  - condition: template
    value_template: "\n{{ is_state('binary_sensor.car1_heating_auto_1','on')\n           or
      is_state('binary_sensor.car1_heating_auto_2','on')\n           or is_state('binary_sensor.car1_heating_auto_3','on')
      }}\n"
  actions:
  - type: turn_on
    device_id: 663b764282a0302b367aafd0ee71a0a1
    entity_id: 6773650fe663c46e1d1c0b7c4c0cf223
    domain: switch
  mode: single

- id: '1776106025022'
  alias: Autolammitys A profile 1
  description: ''
  triggers:
  - trigger: state
    entity_id:
    - input_boolean.autostart_1_1_enabled
    - input_datetime.autostart_1_1
    - input_boolean.autostart_1_1_mon
    - input_boolean.autostart_1_1_tue
    - input_boolean.autostart_1_1_wed
    - input_boolean.autostart_1_1_thu
    - input_boolean.autostart_1_1_fri
    - input_boolean.autostart_1_1_sat
    - input_boolean.autostart_1_1_sun
  conditions: []
  actions:
  - action: script.car_heating_1_1
    metadata: {}
    data: {}
  mode: single

- id: '1776114730447'
  alias: Autolammitys A recalc profile 1
  description: ''
  triggers:
  - trigger: state
    entity_id:
    - sensor.outdoor_temperature_min
  conditions:
  - condition: state
    entity_id: binary_sensor.car1_heating_auto_1
    state:
    - 'off'
  actions:
  - action: script.car_heating_1_1
    metadata: {}
    data: {}
  mode: single

- id: '1776115891730'
  alias: Autolammitys A Auto OFF
  triggers:
  - minutes: /1
    trigger: time_pattern
  - trigger: state
    entity_id:
    - binary_sensor.car1_heating_auto_1
    - binary_sensor.car1_heating_auto_2
    - binary_sensor.car1_heating_auto_3
  conditions:
  - condition: template
    value_template: "{{ is_state('binary_sensor.car1_heating_auto_1','off')\n   and
      is_state('binary_sensor.car1_heating_auto_2','off')\n   and is_state('binary_sensor.car1_heating_auto_3','off')\n}}\n"
  - condition: device
    type: is_on
    device_id: 663b764282a0302b367aafd0ee71a0a1
    entity_id: 6773650fe663c46e1d1c0b7c4c0cf223
    domain: switch
  actions:
  - choose:
    - conditions:
      - condition: state
        entity_id: input_boolean.car1_heating_manual
        state: 'off'
      sequence:
      - type: turn_off
        device_id: 663b764282a0302b367aafd0ee71a0a1
        entity_id: 6773650fe663c46e1d1c0b7c4c0cf223
        domain: switch
      - target:
          entity_id: input_boolean.car1_heating_auto
        action: input_boolean.turn_off
    default:
    - target:
        entity_id: input_boolean.car1_heating_auto
      action: input_boolean.turn_off
  mode: single

- id: '1776119725487'
  alias: Autolammitys A Auto/Manual + time
  description: ''
  triggers:
  - trigger: state
    entity_id:
    - input_number.manual_heat_duration_1
  - type: turned_on
    device_id: 663b764282a0302b367aafd0ee71a0a1
    entity_id: 6773650fe663c46e1d1c0b7c4c0cf223
    domain: switch
    trigger: device
  - trigger: state
    entity_id:
    - binary_sensor.car1_heating_auto_1
    - binary_sensor.car1_heating_auto_2
    - binary_sensor.car1_heating_auto_3
    to:
    - 'on'
  conditions: []
  actions:
  - action: script.car1_heating_mode_and_manual_shutdown
    metadata: {}
    data: {}
  mode: single

- id: '1776122597337'
  alias: Autolammitys A Manual OFF
  description: ''
  triggers:
  - minutes: /1
    trigger: time_pattern
  conditions:
  - condition: template
    value_template: "{{ is_state('binary_sensor.car1_heating_auto_1','off')\n   and
      is_state('binary_sensor.car1_heating_auto_2','off')\n   and is_state('binary_sensor.car1_heating_auto_3','off')
      }}\n"
    enabled: false
  - condition: state
    entity_id: input_boolean.car1_heating_manual
    state: 'on'
  - condition: template
    value_template: "{{ now() >= states('input_datetime.car1_manual_shutdown')\n   |
      as_datetime | as_local }}\n"
  actions:
  - choose:
    - conditions:
      - condition: state
        entity_id: input_boolean.car1_heating_auto
        state: 'off'
      sequence:
      - target:
          entity_id: switch.autolammitys_a
        action: switch.turn_off
      - target:
          entity_id: input_boolean.car1_heating_manual
        action: input_boolean.turn_off
    default:
    - target:
        entity_id: input_boolean.car1_heating_manual
      action: input_boolean.turn_off
  mode: single

- id: '1776188401393'
  alias: Autolammitys A profile 1 oneshot
  description: ''
  triggers:
  - trigger: state
    entity_id:
    - binary_sensor.car1_heating_auto_1
    to:
    - 'on'
  conditions:
  - condition: template
    value_template: "{{ is_state('input_boolean.autostart_1_1_mon','off')\n             and
      is_state('input_boolean.autostart_1_1_tue','off')\n             and is_state('input_boolean.autostart_1_1_wed','off')
      \n             and is_state('input_boolean.autostart_1_1_thu','off')\n             and
      \ is_state('input_boolean.autostart_1_1_fri','off')\n             and\nis_state('input_boolean.autostart_1_1_sat','off')\n
      \            and\nis_state('input_boolean.autostart_1_1_sun','off')}}"
  actions:
  - target:
      entity_id: input_boolean.autostart_1_1_enabled
    action: input_boolean.turn_off
  mode: single

There is also some work done in the templates:

  - sensor:

      # ============================================================
      # TEMPLATE 1: Kylmin ulkolämpötila
      # ============================================================
      - name: "Outdoor Temperature Min"
        unit_of_measurement: "°C"
        icon: mdi:snowflake
        state: >
          {% set t1 = states('sensor.hue_outdoor_motion_sensor_1_temperature') | float(99) %}
          {% set t2 = states('sensor.hue_outdoor_motion_sensor_2_temperature') | float(99) %}
          {% set t3 = states('sensor.temperature') | float(99) %}
          {% set t4 = states('sensor.forecasted_min_5h_temperature') | float(99) %}
          {{ [t1, t2, t3, t4] | min }}

      # ============================================================
      # Car x / profile y
      # ============================================================
      - name: "car_x_heat_start_y"
        state: "{{ states('input_datetime.carx_heat_start_y') }}"

      - name: "car_1_duration_1"
        state: "{{ states('input_number.carx_heat_duration_y') }}"

     
      # ============================================================
      # Car x / Heat End (dep_dt + 30 min)
      # ============================================================
      - name: "car_x_heat_end_y"
        unique_id: car_x_heat_end_y
        icon: mdi:timer-end
        device_class: timestamp
        state: >
          
            
            {% set dep = states('input_datetime.carx_dep_dt_y') %}
            {% if dep not in ['unknown', 'unavailable', ''] %}
                {{ (dep | as_datetime + timedelta(minutes=30))
                    | as_timestamp
                    | timestamp_utc }}
            {% else %}
                unknown
            {% endif %}

      - name: "car_x_heat_end_manual"
        unique_id: car_x_heat_end_manual
        icon: mdi:timer-end
        state: "{{ as_datetime(states('input_datetime.carx_manual_shutdown')).strftime('%Y-%m-%d %H:%M:%S') }}"
          
        
      - name: "Carx Heating Reason"
        unique_id: car_x_heat_reason
        state: >
          {% if is_state('input_boolean.carx_heating_manual','on') %}
            Manuaalinen
          {% elif is_state('binary_sensor.carx_heating_auto_1','on') %}
            Profiili 1
          {% elif is_state('binary_sensor.carx_heating_auto_2','on') %}
            Profiili 2
          {% elif is_state('binary_sensor.carx_heating_auto_3','on') %}
            Profiili 3
          {% else %}
            Ei lämmitystä
          {% endif %}
  


  - binary_sensor:

      # ============================================================
      # CAR x – Heating Auto y
      # ============================================================
      - name: "Carx Heating Auto y"
        unique_id: carx_heating_autoy
        state: >
          {% set now_t = as_timestamp(now()) %}
          {% set start = states('input_datetime.carx_heat_start_y') | as_timestamp %}
          {% set dep   = states('input_datetime.carx_dep_dt_y') | as_timestamp %}
          {% set end   = dep + 30*60 %}
          {% set current = states('sensor.shelly1pmg4_xxxxxxx_current') | float(0) %}

          {% if start <= now_t < end %}
            {% if now_t < dep %}
              true
            {% else %}
              {{ current > 0.1 }}
            {% endif %}
          {% else %}
            false
          {% endif %}

Also did little changes to UI:

type: vertical-stack
title: Lämmityspistorasia A
cards:
  - type: entities
    title: Ulkolämpötilat & ennuste
    show_header_toggle: false
    entities:
      - entity: sensor.outdoor_temperature_min
        name: Yhdistetty alin (reaali + ennuste)
        icon: mdi:snowflake
      - type: section
        label: Ennusteet
      - entity: sensor.temperature
        name: Nykyinen
        icon: mdi:temperature-celsius
      - entity: sensor.forecasted_min_5h_temperature
        name: Alin (5h)
        icon: mdi:weather-snowy
      - type: section
        label: Anturit
      - entity: sensor.hue_outdoor_motion_sensor_1_temperature
        name: Ulkoanturi 1
        icon: mdi:thermometer
      - entity: sensor.hue_outdoor_motion_sensor_2_temperature
        name: Ulkoanturi 2
        icon: mdi:thermometer
  - type: entities
    title: Manuaalinen lämmitys
    entities:
      - entity: sensor.car1_heating_reason
        name: Lämmityksen tila
        icon: mdi:information-outline
      - entity: switch.autolammitys_a
        name: Lämmitys päälle/pois
      - entity: input_number.manual_heat_duration_1
        name: Lämmityksen kesto (h)
      - entity: sensor.car_1_heat_end_manual
        name: Lopetusaika
        icon: mdi:clock-end
      - entity: sensor.shelly1pmg4_xxxxx_current
        name: Virta (A)
  - type: entities
    title: Profiili 1
    entities:
      - entity: input_boolean.autostart_1_1_enabled
        name: Profiili käytössä
      - entity: input_datetime.autostart_1_1
        name: Lähtöaika
      - entity: sensor.car_1_heat_start_1
        name: Aloitusaika
        icon: mdi:clock-start
      - entity: sensor.car_1_duration_1
        name: Lämmitysaika (h)
        icon: mdi:sun-clock
      - type: section
        label: Toistuvat päivät
      - entity: input_boolean.autostart_1_1_mon
        name: Maanantai
      - entity: input_boolean.autostart_1_1_tue
        name: Tiistai
      - entity: input_boolean.autostart_1_1_wed
        name: Keskiviikko
      - entity: input_boolean.autostart_1_1_thu
        name: Torstai
      - entity: input_boolean.autostart_1_1_fri
        name: Perjantai
      - entity: input_boolean.autostart_1_1_sat
        name: Lauantai
      - entity: input_boolean.autostart_1_1_sun
        name: Sunnuntai
    show_header_toggle: false
  - type: entities
    title: Profiili 2
    entities:
      - entity: input_boolean.autostart_1_2_enabled
        name: Profiili käytössä
      - entity: input_datetime.autostart_1_2
        name: Lähtöaika
      - entity: sensor.car_1_heat_start_2
        name: Aloitusaika
        icon: mdi:clock-start
      - entity: sensor.car_1_duration_2
        name: Lämmitysaika (h)
        icon: mdi:sun-clock
      - type: section
        label: Toistuvat päivät
      - entity: input_boolean.autostart_1_2_mon
        name: Maanantai
      - entity: input_boolean.autostart_1_2_tue
        name: Tiistai
      - entity: input_boolean.autostart_1_2_wed
        name: Keskiviikko
      - entity: input_boolean.autostart_1_2_thu
        name: Torstai
      - entity: input_boolean.autostart_1_2_fri
        name: Perjantai
      - entity: input_boolean.autostart_1_2_sat
        name: Lauantai
      - entity: input_boolean.autostart_1_2_sun
        name: Sunnuntai
    show_header_toggle: false
  - type: entities
    title: Profiili 3
    entities:
      - entity: input_boolean.autostart_1_3_enabled
        name: Profiili käytössä
      - entity: input_datetime.autostart_1_3
        name: Lähtöaika
      - entity: sensor.car_1_heat_start_3
        name: Aloitusaika
        icon: mdi:clock-start
      - entity: sensor.car_1_duration_3
        name: Lämmitysaika (h)
        icon: mdi:sun-clock
      - type: section
        label: Toistuvat päivät
      - entity: input_boolean.autostart_1_3_mon
        name: Maanantai
      - entity: input_boolean.autostart_1_3_tue
        name: Tiistai
      - entity: input_boolean.autostart_1_3_wed
        name: Keskiviikko
      - entity: input_boolean.autostart_1_3_thu
        name: Torstai
      - entity: input_boolean.autostart_1_3_fri
        name: Perjantai
      - entity: input_boolean.autostart_1_3_sat
        name: Lauantai
      - entity: input_boolean.autostart_1_3_sun
        name: Sunnuntai
    show_header_toggle: false

I made some more changes :slightly_smiling_face: mainly bug fixes and improvements.

There was a feature where even if you turned a profile off, it would still start heating if the heating start time had already been calculated. Also, it would overwrite manual heating if automatic heating started while manual heating was active.

So I moved the departure and start times to the past if the profile is off:

car_heating_1_1:
  alias: Car1P1 Heating
  mode: single
  sequence:

    # =========================================================
    # VARIABLES
    # =========================================================
    - variables:

        dep_time: "{{ states('input_datetime.autostart_1_1')[0:5] }}"

        enabled: "{{ is_state('input_boolean.autostart_1_1_enabled', 'on') }}"

        duration_entity: "input_number.car1_heat_duration_1"
        start_entity: "input_datetime.car1_heat_start_1"
        dep_entity: "input_datetime.car1_dep_dt_1"
        heating_sensor: "binary_sensor.car1_heating_auto_1"

        sensor_temp: "sensor.outdoor_temperature_min"

        weekdays:
          - "{{ states('input_boolean.autostart_1_1_mon') }}"
          - "{{ states('input_boolean.autostart_1_1_tue') }}"
          - "{{ states('input_boolean.autostart_1_1_wed') }}"
          - "{{ states('input_boolean.autostart_1_1_thu') }}"
          - "{{ states('input_boolean.autostart_1_1_fri') }}"
          - "{{ states('input_boolean.autostart_1_1_sat') }}"
          - "{{ states('input_boolean.autostart_1_1_sun') }}"

        now_ts: "{{ as_timestamp(now()) }}"

    # =========================================================
    # FIND NEXT VALID DEPARTURE
    # =========================================================
    - variables:
        dep_ts: >
          {% set time = dep_time %}
          {% set days = weekdays %}
          {% set any_days = days | select('eq','on') | list | length > 0 %}
          {% set ns = namespace(found=none) %}

          {% for offset in range(0, 14) %}
            {% set candidate = today_at(time) + timedelta(days=offset) %}
            {% set ts = as_timestamp(candidate) %}

            {% if ts > now_ts %}
              {% set dow = candidate.weekday() %}
              {% if not any_days or days[dow] == 'on' %}
                {% set ns.found = ts %}
                {% break %}
              {% endif %}
            {% endif %}
          {% endfor %}

          {{ ns.found }}
          
    # =========================================================
    # DISABLE LOGIC (FORCE PAST START TIME)
    # =========================================================
    - if:
        - condition: template
          value_template: >
            {% set start = states(start_entity) %}
            {{ not enabled
               and states(heating_sensor) == 'off'
               and start not in ['unknown','unavailable','']
               and as_timestamp(start | as_datetime | as_local) > now_ts }}
      then:
        - service: input_datetime.set_datetime
          target:
            entity_id: "{{ start_entity }}"
          data:
            datetime: >
              {{ (now() - timedelta(days=2)).strftime('%Y-%m-%d %H:%M:%S') }}

        - service: input_datetime.set_datetime
          target:
            entity_id: "{{ dep_entity }}"
          data:
            datetime: >
              {{ (now() - timedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S') }}
              
    # =========================================================
    # GUARD (PROFILE ACTIVE)
    # =========================================================
    - if:
        - condition: template
          value_template: >
            {{ enabled and dep_ts is not none }}
      then:

        # =====================================================
        # HEAT DURATION
        # =====================================================
        - variables:
            heat_h: >
              {% set outdoor = states(sensor_temp) | float(999) %}
              {% set e = 2.718281828459045 %}
              {% set val = 0.5 * (e ** (0.075 * (4 - outdoor))) %}
              {{ [4, [0.5, val] | max] | min | round(2) }}

        # =====================================================
        # SAVE DURATION
        # =====================================================
        - service: input_number.set_value
          target:
            entity_id: "{{ duration_entity }}"
          data:
            value: "{{ heat_h }}"

        # =====================================================
        # START TIME
        # =====================================================
        - service: input_datetime.set_datetime
          target:
            entity_id: "{{ start_entity }}"
          data:
            datetime: >
              {{ (dep_ts - (heat_h * 3600))
                 | timestamp_custom('%Y-%m-%d %H:%M:%S', true) }}

        # =====================================================
        # DEPARTURE TIME
        # =====================================================
        - service: input_datetime.set_datetime
          target:
            entity_id: "{{ dep_entity }}"
          data:
            datetime: >
              {{ dep_ts
                 | timestamp_custom('%Y-%m-%d %H:%M:%S', true) }}

I separated heating mode and manual heating shutdown calculations into different scripts:

carx_heating_mode:
  alias: Carx Heating Mode
  mode: single
  sequence:

    # =========================================================
    # INPUTS
    # =========================================================
    - variables:
        auto_sensor_1: "binary_sensor.carx_heating_auto_1"
        auto_sensor_2: "binary_sensor.carx_heating_auto_2"
        auto_sensor_3: "binary_sensor.carx_heating_auto_3"

        master_switch: "switch.autolammitys_x"

        auto_mode_entity: "input_boolean.carx_heating_auto"
        manual_mode_entity: "input_boolean.carx_heating_manual"
        shutdown_entity: "input_datetime.carx_manual_shutdown"

    # =========================================================
    # MASTER SWITCH CHECK
    # =========================================================
    - if:
        - condition: template
          value_template: >
            {{ not is_state(master_switch, 'on') }}

      then:
        # -----------------------------------------------------
        # POWER OFF
        # -----------------------------------------------------
        - service: input_boolean.turn_off
          target:
            entity_id: "{{ auto_mode_entity }}"

        - service: input_boolean.turn_off
          target:
            entity_id: "{{ manual_mode_entity }}"

        - stop: "Switch OFF → no calculation"

    # =========================================================
    # AUTO DETECTION
    # =========================================================
    - variables:
        is_auto: >
          {{ is_state(auto_sensor_1, 'on')
             or is_state(auto_sensor_2, 'on')
             or is_state(auto_sensor_3, 'on') }}

    # =========================================================
    # MODE SET
    # =========================================================
    - choose:
        - conditions:
            - condition: template
              value_template: "{{ is_auto }}"
          sequence:
            - service: input_boolean.turn_on
              target:
                entity_id: "{{ auto_mode_entity }}"

#            - service: input_boolean.turn_off
#              target:
#                entity_id: "{{ manual_mode_entity }}"

      default:
        - service: input_boolean.turn_off
          target:
            entity_id: "{{ auto_mode_entity }}"

        - service: input_boolean.turn_on
          target:
            entity_id: "{{ manual_mode_entity }}"

carx_manual_shutdown_time:
  alias: Carx Shutdown Calculator
  mode: single
  sequence:

    # =========================================================
    # INPUTS
    # =========================================================
    - variables:
        auto_sensor_1: "binary_sensor.carx_heating_auto_1"
        auto_sensor_2: "binary_sensor.carx_heating_auto_2"
        auto_sensor_3: "binary_sensor.carx_heating_auto_3"

        duration_entity: "input_number.manual_heat_duration_x"
        shutdown_entity: "input_datetime.carx_manual_shutdown"

    # =========================================================
    # AUTO CHECK
    # =========================================================
    - if:
        - condition: template
          value_template: >
            {{ not (is_state(auto_sensor_1, 'on')
                or is_state(auto_sensor_2, 'on')
                or is_state(auto_sensor_3, 'on')) }}

      then:
        - variables:
            heat_h: "{{ states(duration_entity) | float(0) }}"

            shutdown_dt: >
              {{ (now() + timedelta(hours=heat_h))
                 .strftime('%Y-%m-%d %H:%M:%S') }}

        - service: input_datetime.set_datetime
          target:
            entity_id: "{{ shutdown_entity }}"
          data:
            datetime: "{{ shutdown_dt }}"

And that also affected the automations:

- id: '1776119725487'
  alias: Autolammitys A Auto/Manual
  description: ''
  triggers:
  - trigger: state
    entity_id:
    - binary_sensor.car1_heating_auto_1
    - binary_sensor.car1_heating_auto_2
    - binary_sensor.car1_heating_auto_3
  - trigger: state
    entity_id:
    - switch.autolammitys_a
  conditions: []
  actions:
  - action: script.car1_heating_mode
    metadata: {}
    data: {}
  mode: single

Manual off will now just turn the manual heating bit off (not the actual heating).

- id: '1776122597337'
  alias: Autolammitys A Manual OFF
  description: ''
  triggers:
  - minutes: /1
    trigger: time_pattern
  conditions:
  - condition: state
    entity_id: input_boolean.car1_heating_manual
    state: 'on'
  - condition: template
    value_template: "{{ now() >= states('input_datetime.car1_manual_shutdown')\n   |
      as_datetime | as_local }}\n"
  actions:
  - target:
      entity_id: input_boolean.car1_heating_manual
    action: input_boolean.turn_off
    data: {}
  mode: single

So the automation Autolammitys A Auto OFF will now actually turn heating off even during manual heating.

- id: '1776115891730'
  alias: Autolammitys A Auto OFF
  triggers:
  - minutes: /1
    trigger: time_pattern
  - trigger: state
    entity_id:
    - binary_sensor.car1_heating_auto_1
    - binary_sensor.car1_heating_auto_2
    - binary_sensor.car1_heating_auto_3
    - input_boolean.car1_heating_manual
  conditions:
  - condition: template
    value_template: "{{ is_state('binary_sensor.car1_heating_auto_1','off')\n   and
      is_state('binary_sensor.car1_heating_auto_2','off')\n   and is_state('binary_sensor.car1_heating_auto_3','off')\n
      \  and is_state('input_boolean.car1_heating_manual','off')\n}}\n"
  - condition: device
    type: is_on
    device_id: 663b764282a0302b367aafd0ee71a0a1
    entity_id: 6773650fe663c46e1d1c0b7c4c0cf223
    domain: switch
  actions:
  - action: switch.turn_off
    metadata: {}
    target:
      entity_id: switch.autolammitys_a
    data: {}
  mode: single

And an automation to trigger manual heating time calculation:

- id: '1776277188921'
  alias: Autolammitys A manual heat time
  description: ''
  triggers:
  - trigger: state
    entity_id:
    - input_number.manual_heat_duration_1
  - trigger: state
    entity_id:
    - switch.autolammitys_a
    enabled: true
  conditions:
  - condition: template
    value_template: "{{ is_state('binary_sensor.car1_heating_auto_1', 'off')\n   and
      is_state('binary_sensor.car1_heating_auto_2', 'off')\n   and is_state('binary_sensor.car1_heating_auto_3',
      'off') }}"
  actions:
  - action: script.car1_manual_shutdown_time
    metadata: {}
    data: {}
  mode: single

I also changed the template a little bit to show if more than one heating source is active:

- name: "Car1 Heating Reason"
        unique_id: car_1_heat_reason
        state: >
            {% set reasons = [] %}

            {% if is_state('input_boolean.car1_heating_manual','on') %}
                {% set reasons = reasons + ['M'] %}
            {% endif %}

            {% if is_state('binary_sensor.car1_heating_auto_1','on') %}
                {% set reasons = reasons + ['P1'] %}
            {% endif %}

            {% if is_state('binary_sensor.car1_heating_auto_2','on') %}
                {% set reasons = reasons + ['P2'] %}
            {% endif %}

            {% if is_state('binary_sensor.car1_heating_auto_3','on') %}
                {% set reasons = reasons + ['P3'] %}
            {% endif %}

            {{ reasons | join(' + ') if reasons | length > 0 else 'Ei lämmitystä' }}

I think that’s all. Now it’s mainly just testing and small changes. I’m sure there is still room for optimization, but maybe someday :slightly_smiling_face: