ESP32 - PWM relay/heater control

Hi

I’ve just bought my 1st ESP 32 board and I’m looking to set it up with PWM to control a solid state relay to control an immersion heater. The heater is rated 3kw and I would like to be able to vary its power based on the level of exported power from my solar panels.

Most of the example code I’ve found relates to fan speed control, it makes sense to me but it seems to be missing anything in the ESPhome code to define the power level.

I also looked at slow_PWM but unless I’m misunderstanding the documentation slow_pwm uses a fixed duty cycle?

Any pointers on how to set this up would be hugely appreciated.

Thanks
Grant

3 Likes

A you certain that could work? I would expect you need a AC dimmer (which is kind of a “pimped” SSR with ZC):

If I’m not mistaken then a solid state relay has an on or off state.

Yes it does.

I was lead to believe that by toggling the relay on & off using the pwm capability of the esp32 it could effectively work as a dimmer.

IE if I want to run the heater at 1.5kw then it would run at 50% or on for X seconds/milliseconds on for Y seconds/milliseconds depending on the export power that the inverter is reporting to HA.

Hi I’m seeking for a similar answer.

I want to adjust the power of an electric boiler via a Kemo M028 and M150.
https://www.kemo-electronic.de/en/Transformer-Dimmer/Converter/M150-DC-pulse-converter.php

https://www.kemo-electronic.de/en/Light-Sound/Effects/Modules/M028N-Power-control-110-240-V-AC-4000-VA.php
The M150 expects TTL rectangular pulses 5 V/DC, 1 - 10 kHz, pulse width approx. 10 - 90% PWM (Puls width modulation). Regulation is done by changing the pulse width.
I have an ESP32 board but as the GPIO outputs are working on 3.3V I also need a logical level converter to 5V.

I have all done, but I’m not familiar with the specific yaml code to start sending PWM signals to GPIO4 compatebel with the M150.
Next I want to adjust the PWM signal based on the numeric value of a helper input_number.injectie.

Hey, iI am looking for the exact same thing. But I will use a Kemo M240 because it already has a pwm controller.

I ended up just configuring the esp32 as a dimmable light.

All I need to do now is work out how to write a script that takes the value from the export power sensor and sets the “lamp” brightness accordingly

1 Like

I forgot that my heating resistors have 2kW in delta connection. I only get 1100W at 230V. That is still sufficient for me.
Here is my test setup. I am having a hard time with the programme. it would be great if someone had helpful tips.

Heating resistor test

Hi, did you proceed somehow? I’m doing same project. So far I can change ssr manually, but I’m looking for automatic setting based on excess PV energy.

is that automatic, or manual?

Me too.

I’ve kinda given up for now.
Starting to get to the time of year when there is minimal excess & my inverter doesn’t update fast enough. So HA will switch on the heater then shut it down when the export power drops off.

Need to add a CT clamp to the esp32 to monitor the load from the heater and factor out in somehow.

Or that might just be my brain trying to over engineer the problem.

I have a heating rod which consists of three individual 230 V heating rods each 1kw. Actually, my goal was to completely dim this 3kw but the dimmer of Robotdyn 24A 600V gets too hot. Thus, this dims only one heating element 1kw. alternatively, +1kw heating can be connected via two ssr.

Dimmer module for 16/24A 600V High Load, 1 Channel, 3.3V/5V logic|Integrated Circuits| - AliExpress

substitutions:
  name: "PV-Heizstab"

esphome:
  name: "pv-heizstab"

esp32:
  board: esp32dev

# Enable logging
logger:
  level: debug

# Enable Home Assistant API
api:

ota:
  
# Enable Web server.
web_server:
  port: 80
  
wifi:
 # use_address: oel-fuellstand-sensor
  networks:
    - ssid: !secret wlan_ssid        
      password: !secret wlan_password    

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "${name} Fallback Hotspot"
    password: "W5ywu1mCj6ta"

captive_portal:

globals:
   - id: my_global_int
     type: int
     restore_value: no
     initial_value: '0'
     

# Manueller Modus oder PV Modus
select:
  - platform: template
    name: "${name}-Betriebsart"
    id: betriebsart
    optimistic: true
    options:
      - Manuell
      - Überschuss
    initial_option: Überschuss
    
number:
  - platform: template
    name: "${name}-Manuelle Soll-Leistung"
    id: man_soll_pwr
    unit_of_measurement: ""
    optimistic: true
    min_value: 0
    max_value: 1
    step: 0.1   
    
    
button:
  - platform: restart
    name: "${name}-Neustart"
    


switch:
  - platform: gpio
    pin: GPIO26
    name: "${name}-Pumpe"

#ssr oben
  - platform: gpio
    pin: GPIO05
    id: heater1
    name: "${name}-Heizung 1"
    inverted: true
#ssr unten
  - platform: gpio
    id: heater2
    pin: GPIO16
    name: "${name}-Heizung 2"
    inverted: true
# AC-Dimmer 
# Dimmer mit Zero Cross 24A max    
output:
  - platform: ac_dimmer
    id: dimmer1
    gate_pin: GPIO13
    zero_cross_pin:
      number: GPIO14
      mode:
        input: true
      inverted: yes
    
#Temperatur Sensoren
dallas:
  - pin: 27
    update_interval: 10sec

#Sensoren  
sensor:
  - platform: dallas
    address: 0x263db8071e64ff28
    name: "${name}-Temperatur"
    id: temp_heizung
  - platform: dallas
    address: 0xd46abe071e64ff28
    name: "${name}-Temperatur Warmwasser"
    id: temp_warmwasser

  # WiFi Signal sensor.
  - platform: wifi_signal
    name: WiFi Signal
    update_interval: 60s
    
#Powerfactor -> Wieviel Leistung wird zugeschaltet
  - platform: template
    name: "${name}-Leistungsfaktor"
    id: pwrfactor
    unit_of_measurement: "%"
    accuracy_decimals: 0
    lambda: |-
      float allowed_power = id(pwrfactor).state/100.0;
      float temp_pwr = 0.0;
      float max_pwr = 1000; 
      int manuell = 0;
      
      if(strcmp(id(betriebsart).state.c_str(), "Manuell")==0){
        allowed_power = id(man_soll_pwr).state;
        manuell =1;
      }
      else{
        temp_pwr = (id(grid_power_supplied).state - 250.0)/max_pwr;
        if(temp_pwr > allowed_power){
          allowed_power = temp_pwr;
          ESP_LOGI("custom", "increase power");
        }
      }
        
      if(
         (id(grid_power_supplied).state < 50.0 && manuell == 0)|| 
         isnan(id(grid_power_supplied).raw_state) > 0 ||
         id(temp_heizung).state > 80.0 ||
         isnan(id(temp_heizung).raw_state) > 0
        )
        {
          id(heater1).turn_off();
          id(heater2).turn_off();
          allowed_power = 0.0;
        }
        
      if(allowed_power > 1.0){
        allowed_power = 1.0;
        }
        //schalten der Ausgänge
        id(dimmer1).set_level(allowed_power);

        if (allowed_power >= 1.0 && id(grid_power_supplied).state > 300.0 && id(temp_heizung).state < 75.0){
          if(!id(heater1).state){
            id(heater1).turn_on();
            id(dimmer1).set_level(0.0);
          }else if (!id(heater2).state){
            id(heater2).turn_on();
            id(dimmer1).set_level(0.0);
          }
          
        } 
        return allowed_power*100.0;
    update_interval: 10s

#Uptime    
  - platform: uptime
    id: uptime_sensor
    update_interval: 60s
    on_raw_value:
      then:
        - text_sensor.template.publish:
            id: uptime_human
            state: !lambda |-
              int seconds = round(id(uptime_sensor).raw_state);
              int days = seconds / (24 * 3600);
              seconds = seconds % (24 * 3600);
              int hours = seconds / 3600;
              seconds = seconds % 3600;
              int minutes = seconds /  60;
              seconds = seconds % 60;
              return (
                (days ? to_string(days) + "d " : "") +
                (hours ? to_string(hours) + "h " : "") +
                (minutes ? to_string(minutes) + "m " : "") +
                (to_string(seconds) + "s")
              ).c_str();

    
#PV Überschuss von Homeassistant
  - platform: homeassistant
    id: grid_power_supplied
    name: "${name}-PV Überschuss"
    entity_id: sensor.grid_power_supplied
    internal: false
    unit_of_measurement: W
    accuracy_decimals: 0
    on_value:
      then:
        - component.update: pwrfactor    
#Binary Sensoren
binary_sensor:
  - platform: status
    name: "${name}-Status"
    
# Text Sensoren
text_sensor:
  - platform: template
    name: "${name}-Uptime"
    id: uptime_human
    icon: mdi:clock-start
    
  # Expose ESPHome version as sensor.
  - platform: version
    name: ESPHome Version
  # Expose WiFi information as sensors.
  - platform: wifi_info
    ip_address:
      name: IP
    ssid:
      name: SSID
    bssid:
      name: BSSID    
     

My first plan was to just switch on the load in 1kw steps.

1 Like

Ah now I’m remembering.

The issue was that the SSR setting isn’t reflected in the house load.

So 100% “brightness” will see 3kw
50% may only see 800w

I don’t know if that’s because the SSRs control voltage is 4v and it’s only getting 3.3 or if it’s that the inverter only updates the load every 30 seconds and so if only showing what is seeing based on the point in the pwm that it takes its snapshot.

I tried to switch to slow pwm but couldn’t work out how to change the duty cycle from home assistant.

PWM without ZeroCrossing detection will not work 100% as a dimmer in AC

Had to look that up.
Found a thread on stack exchange that explained it pretty well.

It also mentioned slow pwm for heater control.
So is that an option?

Yes, of course, this process should be automated at some point. For this I still need a bit of code. That’s why I created this provisional page for testing.

I read this somewhere before :thinking:

…it was end of august in exact this thread :joy:

Without zero crossing? No, not reliable because you never know which part of the wave :wavy_dash: you are manipulating (“turning off”).

Thanks.

I know the guy who gave me advice on parts has a second esp32 with a CT monitoring his import/export power. I guess he’s using that and not to bothered about the zero crossing.

With the slow pwm surely it would negate the issue slightly if say to get ~1kw from a 3kw heater it switches on for 1 second then off 2 then even if it’s not zero it’s not as much of an issue as it would be if the pwm was cycling the relay at say 40khz?

Four months in to using HA, I’m at the stage of trying to achieve this too, as a means of efficiently using surplus solar energy. I’m not convinced any kind of PWM control will actually reduce the energy that my electricity meter will bill me for, though.

Let’s say I know my solar system is exporting a surplus of 1.5kW, so I’d like to have my 3kW immersion heater use it. If I switch it on and off with an SSR to achieve 50% PWM, however rapidly, it’s still going to alternately draw 3kW, then 0kW. So my meter will see bursts of +1.5 and -1.5kW, and I’ll still get billed for the positive bursts and be giving away the negative ones without using them.

Or am I missing something about how the meter measures PWM draws?

1 Like

have a look at this project - he has done all the heavy lifting for you