Thermostat 3 Steps controller for Sauna

Hello all,

I have a saunaoven with 3x 2000W resistors. I’m building a sauna controller with ESPhome…

I would like that the heater goes full power up to the desired temperature when switching on, then it should continue with 1x 2000W.

If now the temperature goes down by 1 degree, a second heating resistor with 2000W should go on. If the temperature goes down by 2 degrees, the third resistor should go on, so the oven heats with full power at 3x 2000W again to reach the desired temperature.

If the temperature goes up by 1 degree, the heater should fully stop until it falls down to the desired temperature again and start heater by 1x 2000W.

I have a relay board so echt resistor can be switched individually.

I think it’s no really complicated but I didn’t find a way out. Can anyone help my finding the right code?

Here is what my actual code looks like. The section “heat_action” was only a short test now…:

captive_portal:

dashboard_import:
  package_import_url: github://esphome/example-configs/esphome-web/esp32.yaml@main
  import_full_config: true

# Sets up Bluetooth LE (Only on ESP32) to allow the user
# to provision wifi credentials to the device.
esp32_improv:
  authorizer: none

# To have a "next url" for improv serial
web_server:


sensor:
 - platform: dallas_temp
   name: Temperatur
   id: temperatur
   update_interval: 1s
 - platform: homeassistant
   entity_id: input_number.sauna_zeit
   id: sauna_zeit
   name: "Sauna Zeit"
   internal: true
 - platform: adc
   id: temp_sensor
   pin: A0
   update_interval: 1s
 - platform: resistance
   sensor: temp_sensor
   configuration: DOWNSTREAM
   resistor: 108kOhm
   id: resistance_sensor
 - platform: ntc
   sensor: resistance_sensor
   calibration:
     b_constant: 3950
     reference_temperature: 25°C
     reference_resistance: 100kOhm
   name: NTC Temperature

one_wire:
  - platform: gpio
    pin: GPIO13

switch:
  - platform: gpio
    name: "Relay light"
    id: light_switch
    pin:
      number: GPIO15
      inverted: true
  - platform: gpio
    name: "Relay Ofen 1"
    id: ofen1
    pin:
      number: GPIO02
      inverted: true
  - platform: gpio
    name: "Relay Ofen 2"
    id: ofen2
    pin:
      number: GPIO00
      inverted: true
  - platform: gpio
    name: "Relay Ofen 3"
    id: ofen3
    pin:
      number: GPIO04
      inverted: true


climate:
  - platform: thermostat
    visual:
      min_temperature: 10
      max_temperature: 100
      temperature_step: 1
    name: "Thermostat Climate Controller"
    id: saunasteuerung
    sensor: temperatur
    min_heating_off_time: 1s
    min_heating_run_time: 1s
    min_idle_time: 1s
    heat_action:
      then:
        - if:
            condition:
              lambda: return id(saunasteuerung).current_temperature < id(saunasteuerung).target_temperature_low;
            then:
              - switch.turn_on: ofen1
              - switch.turn_on: ofen2
              - switch.turn_on: ofen3
            else:
              - if:
                  condition:
                    lambda: return id(saunasteuerung).current_temperature >= id(saunasteuerung).target_temperature_low;
                  then:
                    - switch.turn_on: ofen1
    idle_action:
      - switch.turn_off: ofen1
      - switch.turn_off: ofen2
      - switch.turn_off: ofen3
    default_preset: Home
    preset:
      - name: Home
        default_target_temperature_low: 90 °C
            

globals:
  - id: shutdown_timer
    type: int
    initial_value: '0'


binary_sensor:
  - platform: gpio
    pin:
      number: GPIO27
      mode: INPUT_PULLUP
      inverted: True
    name: AN/AUS
    on_press:
      then:
        - if:
            condition:
              lambda: return id(saunasteuerung).mode == CLIMATE_MODE_OFF;
            then:
              - climate.control:
                  id: saunasteuerung
                  mode: 'HEAT'
              - globals.set:
                  id: shutdown_timer
                  value: '7200'  # Zeit in Sekunden
            else:
              - if:
                  condition:
                    lambda: return id(saunasteuerung).mode == CLIMATE_MODE_HEAT;
                  then:
                    - climate.control:
                        id: saunasteuerung
                        mode: 'OFF'
                    - globals.set: 
                        id: shutdown_timer
                        value: '0'

  - platform: gpio
    pin:
      number: GPIO25
      mode: INPUT_PULLUP
      inverted: True
    id: light
    name: "Light"
    on_press:
      - switch.toggle: light_switch

  - platform: gpio
    pin:
      number: GPIO14
      mode: INPUT_PULLUP
      inverted: True
    id: temp_minus
    name: "Temp -"
    on_press:
      - climate.control:
          id: saunasteuerung
          target_temperature: !lambda 'return id(saunasteuerung).target_temperature_low - 1;'

  - platform: gpio
    pin:
      number: GPIO26
      mode: INPUT_PULLUP
      inverted: True
    id: temp_plus
    name: "Temp +"
    on_press:
      - climate.control:
          id: saunasteuerung
          target_temperature: !lambda 'return id(saunasteuerung).target_temperature_low + 1;'

  - platform: gpio
    pin:
      number: GPIO33
      mode: INPUT_PULLUP
      inverted: True
    id: time_plus
    name: "Time +"
    on_press:
      - globals.set:
          id: shutdown_timer
          value: !lambda 'return id(shutdown_timer) + 600;'  # Zeit in Sekunden

  - platform: gpio
    pin:
      number: GPIO12
      mode: INPUT_PULLUP
      inverted: True
    id: time_minus
    name: "Time -"
    on_press:
      - globals.set:
          id: shutdown_timer
          value: !lambda 'return id(shutdown_timer) - 600;'  # Zeit in Sekunden

text_sensor:
  - platform: template
    id: zeit
    name: "Shutdown Timer"
    lambda: |-
      int seconds = id(shutdown_timer);  // Den aktuellen Wert in Sekunden erhalten
      int hours = seconds / 3600;  // Berechnung der Stunden
      int minutes = (seconds % 3600) / 60;  // Berechnung der Minuten
      seconds = seconds % 60;  // Berechnung der verbleibenden Sekunden
      return to_string(hours) + ":" + 
             (minutes < 10 ? "0" : "") + to_string(minutes) + ":" + 
             (seconds < 10 ? "0" : "") + to_string(seconds);  // Formatierung in hh:mm:ss
    update_interval: 1s  # Aktualisiert den Wert jede Sekunde

interval:
  - interval: 1s
    then:
      - if:
          condition:
            - lambda: 'return id(shutdown_timer) > 0;'
          then:
            - globals.set:
                id: shutdown_timer
                value: !lambda 'return id(shutdown_timer) - 1;'
          else:
            - globals.set:
                id: shutdown_timer
                value: "0"
            - climate.control: 
                id: saunasteuerung
                mode: 'OFF'
  - interval: 1s
    then:        
      - if:
          condition:
            lambda: 'return id(saunasteuerung).mode == CLIMATE_MODE_HEAT;'
          then:
            display.page.show: page1
          else:
            display.page.show: page2


font:
    # gfonts://family[@weight]
  - file: "gfonts://Roboto"
    id: roboto_12
    size: 12
  - file: "gfonts://Roboto"
    id: roboto_23
    size: 23
i2c:
  sda: GPIO21
  scl: GPIO22

display:
  - platform: ssd1306_i2c
    model: "SH1106 128x64"
    id: display1
    pages:
      - id: page1
        lambda: |-
         it.printf(0, 0, id(roboto_23),"IST:  %.1f °C", id(temperatur).state);
         it.printf(0, 24, id(roboto_12),"Soll Temp:  %.f °C", id(saunasteuerung).target_temperature);
         it.printf(0, 35, id(roboto_23), "Zeit: %s", id(zeit).state.c_str());

      - id: page2
        lambda: |-
         it.print(0, 0, id(roboto_23), "");

Thanks and best regards!

I got lost in your yaml.
But if you have temp sensor and set temp plus 3 resistors, the automation shouldn’t be complicated: Set the temp sensor to 1deg resolution. Use interval component to update the relays:

if Set - Temp < 0, turn 1 and 2 and 3 off
else if Set - Temp = 1, turn 1 and 2 on, 3 off
else if Set - Temp > 1, turn 1 and 2 and 3 on
else, turn 1 on, 2 and 3 off (difference is 0 within 1degree)

You don’t have to think what the previous situation was, just write all 3 Gpios every time you update. It doesn’t physically do anything if previous state is same than new state.

Anyway, don’t trust too much on cheap relay modules and never ever bypass the original over-temperature switch of your oven.
Also, verify that you don’t use strapping Gpio pins (you didn’t specify what board you use).
And one degree temp difference is very low, your relays will switch on and off unnecessarily often.

Hi Karosm,

thanks for your quick feedback.
I use an ESP32 Wroom 32E Board with DB18S20 temp sensor set to 1 deg resolution.

Of course, I will use overheat switch and I use 3 relays in serial for each resistance to be sure shut off works.

I use the climate/thermostat platform with single point. But it seems if/then does not work in heat_action.
I tried with the supplement_heat_action, but that doesn’t seem to work also witht he delta…

1 deg is only to test, maybe I will put 2-3 deg as delta. And even with a second step (only 2 resistors on). I need to check in practice.

climate:
  - platform: thermostat
    visual:
      min_temperature: 10
      max_temperature: 100
      temperature_step: 1
    name: "Thermostat Climate Controller"
    id: saunasteuerung
    sensor: temperatur
    min_heating_off_time: 1s
    min_heating_run_time: 1s
    min_idle_time: 1s
    heat_action:
      then:
        - if:
            condition:
              lambda: return id(saunasteuerung).current_temperature < id(saunasteuerung).target_temperature_low;
            then:
              - switch.turn_on: ofen1
              - switch.turn_on: ofen2
              - switch.turn_on: ofen3
            else:
              - if:
                  condition:
                    lambda: return id(saunasteuerung).current_temperature >= id(saunasteuerung).target_temperature_low;
                  then:
                    - switch.turn_on: ofen1
    idle_action:
      - switch.turn_off: ofen1
      - switch.turn_off: ofen2
      - switch.turn_off: ofen3
    default_preset: Home
    preset:
      - name: Home
        default_target_temperature_low: 90 °C

Here is what it looks like for now. I put et everything in an old oem controller. All switches works :wink:


You are using lot of pins that might cause problems in certain situations.
If you not short of pins, try to avoid that.
Have a look at the list of Gpios, especially the notes.

Thank for the info. All connected pins are necessary. I began with an esp8266 and moved to esp32 cause I run out of pins.
Everything works so far, everything seems to be fine, I checked with the usecases of the different pins.

The only thing is how I can use the climate or thermostat component as I want, with 2 or 3 action at different temperature deltas?
Or do you think I need to not use the climate component or thermoastat.

Sorry, I’m not very fit in programming. I spend a few hours for this code and got help from chatgpt :sweat_smile:

I finally got it working with your help!
It seems to work perfectly with the interval component.
Will see when first use with real condition in the next days.

Thank you very much for this tip!

You are welcome!