Using ESP to control pump speeds via PWM

@Rachel I’m thinking I might start by loading ESPHome onto an ESP chip of some variety to allow control of the pump speed and expose this in Home Assistant.
Home Assistant is already subscribed to the MQTT topics coming from my OEM flow meter so the next step might be to set something up to steer the pump speed based on this. There is a PID controller HACS integration I might try to do this.

If it turns out that this is to clunky and really I need to be controlling based on a higher frequency of temperature sampling I might have to go down a similar route to you where I connect temperature sensors direct to the ESP and do everything on that.

I’m new to a lot of this stuff although it is dredging up distant memories of doing similar things on PIC Microcontrollers during my electronics degree.

@ajdunlop I think your approach will probably work OK. I didn’t have home assistant when I was thinking about it and I already had an ESP device reading the flow meter, so it seemed like a natural extension. However, the control doesn’t need to be fast as nothing is happening very fast in the system. You will need to add a few bells and whistles to the control though. I use an Arduino PID controller algorithm, but I do have to change how it works depending on whether the heating or DHW is running (as I have two pumps and a LLH). I’ve also just added some code to restrict the minimum pump speed on DHW as I was gettng those strange periods that others have reported where heat output is zero even though the ASHP is drawing a kW or so. Keeping the flow up does seem to have fixed that - I’ll do a post on the relevant thread when I’ve got enough data to convince myself!

@Rachel I’ve just put in the order for all the parts for my ESP journey starts now!

Our system should be a bit simpler as we only have a single pump for heating and DHW.

What do you do about when the pump is running but the heat pump is off? It looks like you do something differently when that happens to avoid the controller putting the pump to max when the ΔT drops to nothing? I guess I could just turn off the PWM signal so the pump reverts to normal operation.

I noticed what your system was doing recently during the DHW dips especially in the colder weather. It looks like a great improvement over the normal behaviour and I wonder if it could solve my woes.

Did you have that problem with your DHW runs before you implemented the PWM control or was it something that appeared when you were controlling the pump but just treating it the same as space heating?

All this begs the question as to why Mitsubishi don’t set the FTC up to control pumps properly if it isn’t particularly hard to implement.

@ajdunlop before I was controlling the pumps I did occaionally get odd things happening on DHW, but then my delta T could be very low (2-3C with a high flow rate) and I think they may have been linked to the starting temperature in the tank, but without exploring in lots of detail I can’t be sure. After I put the pump control on I was getting DHW flows down to 8-9l/m sometimes, below the recommended minimum and that did seem to coincide with the odd behviour. I’ve only set it to a minimum of about 10l/m now, but it hasn;t done it since, so fingers crossed on that.

The way my Ecodan controller is set up, it turns off the circ pump when the heating/DHW isn’t running anyway, so the PWM has no impact. However, I have no antifreeze in my system, so it runs the ASHP side pump as frost protection below about 6C (I don’t undertand why that’s so high - probably need to check a setting somewhere!). That was causing the ASHP pump to ramp up to full speed, but becuase the radiator circuit pump is off then, I was able to detect that from the PWM return signal (which I’m also monitoring on my ESP) and use that to set the ASHP pump to minimum speed when the radiator pump is not running and the system is not on DHW. (As noted, you soon find all the extra bells and whistles needed to make it work properly!)

On the FTC pump control…

I am suprised that the built in DHW/heat ciruit PWM controller appears not to work at all, but I’m still uncertain about controlling pump speed. I do wonder if it’s interfereing with the auto-adapt control - i.e. would the FTC set a different flow temperature in order to try to maintain a constant DT if I wasn’t controlling it? It didn’t seem to do that, but that might be a lack of emitter area meaning that it just couldn;t get there or because my LLH (without the control) meant that the ASHP was never really seeing the proper DT!

Rachel

1 Like

@Rachel I’ve managed to get an ESP32 with ESPHome installed controlling my PWM pump.
I think I might already be seeing an improvement in performance but a bit hard to say as it has just gotten milder.

I’m trying to tune my PID Controller (ESPhome PID Climate) and am working through some guides as to how to do this. Could you share your values? I don’t know how transferable they will be but might be interesting to compare.

Also do you use the same parameters for your controller for DHW and heating? I’m wondering if I need to use different ones as with DHW I’m getting oscillations but not seeing this with heating. It looks like changes in the pump speed have a larger effect on the ΔT for DHW than space heating.

@ajdunlop In my experience, every control system will be different! (I started my life as a control engineer for ICI!) But as a general rule, start with proportional setting of 100%, and a relatvely small amount of integral action. Don’t apply derivative at all - in this type of system I would say never as it’s very likely to oscillate. One problem will be knowing what ‘units’ the PID settings are in as it may be different if you’re using a different PID controller - I’m assuming the module I’m using 100% corresponds to 1.

I’m using PID_V1 in my arduino control PID_v1_bc - Arduino Reference

On my primary pump, the Settings are P=7.5, I=20 and on my Radiator circuit pump P=15 and I = 20.

Note that I’m also setting output limits to avoid controller wind-up - 25 to 178 on the primary pump (which corresponds to the fraction of 255 for the PWM signal. 178 limits it to 70% or about 10l/s. On the radiator pump I have the full range 25-230 set as my output limits.

Having 2 pumps might be helping me with the DHW issue becuase the circuit isn’t wildly different in response time between circulating to the LLH or circulating to the DHW tank. But the PID_V1 module does allow you to dynamically change the settings if you need to.

Good luck!
Rachel

The PID controller I’m using has settings 0-1.
I have limits set to stop the flow rate being too low or high. Currently this just limits the output of the controller but I’m wondering if it would be better to scale the output so 0% is my min and 100% is my max.
I’m fairly sure now I will need to use different settings for Heating and DHW which should be easy enough as the controller can have these dynamically set like yours.
When I get one working the other is less good.
I adjusted Proportional upwards and have had this in the lasted DHW run:


What way should I be adjusting from here?

@ajdunlop That response suggests you have too much gain/proprotional action, so defintely reduce that. Do you know what the integral action is doing? Or how it’s specified? The response times on heating will likely be much slower than on DHW, so you’ll want lower P&I action to avoid overshoot. My system is almost over-damped becuase I was worrying about how the heat pump would respond if I did things to quickly - start slow and work up I would suggest.

On the output limits side, the thing to note is that the PWM signal to the pump is only active between 15% and 90%, so you don’t want the output to keep going beyond those limts as you get ‘wind-up’ which will slow the rate at which compensating action starts to happen. And you can’t go below 5% because it’ll turn the pump off!

Here’s mine responding to a heating cycle and DHW cycle:

After a bit of fiddling yesterday this is what I have (Emoncms - app view):

I’ve split out a separate set of controller parameters that get applied when the HP switches to DHW mode. Although you can see a bit of oscillation at the start of the DHW run show, this is because I’m relying on the value being scraped from MelCloud on a 5 minute poll so it takes a bit of time ino the DHW cycle before the correct parameters are applied.
I’m going to be fixing that in the next day or two with local reading of the values.

I think it looks fairly good although there is still a bit of a dip in each heating cycle and the change in flow could maybe react a bit quicker:

Is it the integral value that I would need to change to make that work?

Here is my current esphome configuration, in case anyone finds it useful:

substitutions:
  initial_kp: "0.15"
  initial_ki: "0.001"
  initial_kd: "0.000"
  initial_kp_dhw: "0.100"
  initial_ki_dhw: "0.000"
  initial_kd_dhw: "0.000"

esphome:
  name: cellar-esp32

esp32:
  board: esp32-s3-devkitc-1
  framework:
    type: arduino

# Enable logging
logger:

# Enable Home Assistant API
api:
  password: ""

ota:
  password: ""

wifi:
  ssid: "XXXXX"
  password: "XXXXX"

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "XXXXX"
    password: "XXXXX"

captive_portal:

globals:
  - id: pwd_level
    type: float
    restore_value: yes
    initial_value: "50"

sensor:
  - platform: homeassistant
    name: "DeltaT From Home Assistant"
    entity_id: sensor.heat_meter_deltat
    id: deltat
    on_value_range:
      - below: 0.4
        then:
          - climate.pid.reset_integral_term: pid_controller
  - platform: homeassistant
    name: "Heat Pump Operation Mode"
    entity_id: sensor.emoncms_operationmode
    id: operation_mode
    on_value:
      then:
        - climate.pid.set_control_parameters:
              id: pid_controller
              kp: !lambda |-
                if(id(operation_mode).state != 1) 
                {
                  return id(proportional).state;
                } else {
                  return id(proportional_dhw).state;
                }
              ki: !lambda |-
                if(id(operation_mode).state != 1) {
                  return id(integral).state;
                } else {
                  return id(integral_dhw).state;
                }
              kd: !lambda |-
                if(id(operation_mode).state != 1) {
                  return id(derivative).state;
                } else {
                  return id(derivative_dhw).state;
                }
  - platform: pid
    name: "PID Climate Result"
    type: RESULT
    id: pid_result

number:
  - platform: template
    name: "Minimum System Pump Duty"
    id: min_pump_duty
    initial_value: 0.46
    min_value: 0
    max_value: 1
    step: 0.01
    optimistic: true
  - platform: template
    name: "Maximum System Pump Duty"
    id: max_pump_duty
    initial_value: 0.85
    min_value: 0
    max_value: 1
    step: 0.01
    optimistic: true
  - platform: template
    name: "Default System Pump Duty"
    id: default_pump_duty
    initial_value: 0.58
    min_value: 0
    max_value: 1
    step: 0.01
    optimistic: true
  - platform: template
    name: "Proportional"
    id: proportional
    initial_value: ${initial_kp}
    min_value: 0
    max_value: 1
    step: 0.001
    optimistic: true
    entity_category: config
    restore_value: true
    on_value:
      then:
        if:
          condition:
            lambda: 'return x != 1;'
          then:
            - climate.pid.set_control_parameters:
                id: pid_controller
                kp: !lambda "return x;"
                ki: !lambda "return id(pid_controller).get_ki();"
                kd: !lambda "return id(pid_controller).get_kd();"
  - platform: template
    name: "Integral"
    id: integral
    initial_value: ${initial_ki}
    min_value: 0
    max_value: 1
    step: 0.001
    optimistic: true
    entity_category: config
    restore_value: true
    on_value:
      then:
        if:
          condition:
            lambda: 'return x != 1;'
          then:
           - climate.pid.set_control_parameters:
               id: pid_controller
               kp: !lambda "return id(pid_controller).get_kp();"
               ki: !lambda "return x;"
               kd: !lambda "return id(pid_controller).get_kd();"
  - platform: template
    name: "Derivative"
    id: derivative
    initial_value: ${initial_kd}
    min_value: 0
    max_value: 1
    step: 0.001
    optimistic: true
    entity_category: config
    restore_value: true
    on_value:
      then:
        if:
          condition:
            lambda: 'return x != 1;'
          then:
           - climate.pid.set_control_parameters:
               id: pid_controller
               kp: !lambda "return id(pid_controller).get_kp();"
               ki: !lambda "return id(pid_controller).get_ki();"
               kd: !lambda "return x;"
  - platform: template
    name: "Proportional DHW"
    id: proportional_dhw
    initial_value: ${initial_kp_dhw}
    min_value: 0
    max_value: 1
    step: 0.001
    optimistic: true
    entity_category: config
    restore_value: true
    on_value:
      then:
        if:
          condition:
            lambda: 'return x == 1;'
          then:
           - climate.pid.set_control_parameters:
               id: pid_controller
               kp: !lambda "return x;"
               ki: !lambda "return id(pid_controller).get_ki();"
               kd: !lambda "return id(pid_controller).get_kd();"
  - platform: template
    name: "Integral DHW"
    id: integral_dhw
    initial_value: ${initial_ki_dhw}
    min_value: 0
    max_value: 1
    step: 0.001
    optimistic: true
    entity_category: config
    restore_value: true
    on_value:
      then:
        if:
          condition:
            lambda: 'return x == 1;'
          then:
           - climate.pid.set_control_parameters:
               id: pid_controller
               kp: !lambda "return id(pid_controller).get_kp();"
               ki: !lambda "return x;"
               kd: !lambda "return id(pid_controller).get_kd();"
  - platform: template
    name: "Derivative DHW"
    id: derivative_dhw
    initial_value: ${initial_kd_dhw}
    min_value: 0
    max_value: 1
    step: 0.001
    optimistic: true
    entity_category: config
    restore_value: true
    on_value:
      then:
        if:
          condition:
            lambda: 'return x == 1;'
          then:
           - climate.pid.set_control_parameters:
               id: pid_controller
               kp: !lambda "return id(pid_controller).get_kp();"
               ki: !lambda "return id(pid_controller).get_ki();"
               kd: !lambda "return x;"

climate:
  - platform: pid
    name: "PID deltaT Controller"
    id: pid_controller
    sensor: deltat
    default_target_temperature: 5
    cool_output: system_pump_pwm_calculated
    heat_output: system_pump_pwm_calculated
    control_parameters:
      kp: ${initial_kp}
      ki: ${initial_ki}
      kd: ${initial_kd}
      output_averaging_samples: 3
      derivative_averaging_samples: 1
    deadband_parameters:
      threshold_high: 0.1
      threshold_low: -0.1
    visual:
      min_temperature: 3
      max_temperature: 7
      temperature_step: 0.5

output:
  - platform: ledc
    pin: GPIO19
    frequency: 1000 Hz # also there were values 500 Hz, 1000 Hz, 10000 Hz
    id: system_pump_pwm
    inverted: True
      #    min_power: 52%
      #    max_power: 85%
  - platform: template
    id: system_pump_pwm_calculated
    type: float
    write_action:
      - output.set_level:
          id: system_pump_pwm
          level: !lambda |-
            float level = 0;

            ESP_LOGD("main", "state %f", (state));

            if(id(pid_controller).action == 0) {
              ESP_LOGD("main", "ACTION OFF - Switching to Default Pump Duty");
              level = id(default_pump_duty).state;
            } else if(id(pid_controller).action == 2) {
              ESP_LOGD("main", "ACTION COOLING");
              if ((state) == 0) {
                level = id(pwd_level);
              } else {
                level = (((((state)/2)+0.5) * (id(max_pump_duty).state - id(min_pump_duty).state)) + id(min_pump_duty).state) ;
              }
            } else if(id(pid_controller).action == 3) {
              ESP_LOGD("main", "ACTION HEATING");
              if ((state) == 0) {
                level = id(pwd_level);
              } else {
                level = ((((1-(state))/2) * (id(max_pump_duty).state - id(min_pump_duty).state)) + id(min_pump_duty).state);
              }
            } else {
              ESP_LOGD("main", "ACTION UNKNOWN - Switching to Default Pump Duty");
              level = id(default_pump_duty).state;
            }


            if (level < id(min_pump_duty).state) {
              ESP_LOGD("main", "limiting to minimum pump duty", (id(min_pump_duty).state));
              level = id(min_pump_duty).state;
            } else if (level > id(max_pump_duty).state) {
              ESP_LOGD("main", "limiting to maximum fan duty", (id(max_pump_duty).state));
              level = id(max_pump_duty).state;
            } else {
              ESP_LOGD("main", "value within range", (state));
            }
          

            id(pwd_level) = level;
            ESP_LOGD("main", "value to be sent to pump %f", level);
            return level;

#I hoped this fan component would expose the PWM output in HA but it doesn't appear to update with changes to system_pump_pwm
fan:
  - platform: speed
    output: system_pump_pwm
    name: "System Pump"
    id: system_pump_pwm_fan

Things to note:

  • I am new to ESPHome so I’m sure I haven’t done things in the most efficient way and have misunderstood how some things work. Now that I have things basically working I might start to tidy up the code. Any suggestions anyone more experienced has would be appreciated.
  • The PID Climate component of ESPHome, as I have it configured, outputs a 0-1 value for heating and a 0-1 value for cooling. I only want a single value range so patch them together into a single 0-1 value for both.
  • I have a max and min PWM value worked out to limit my min and max flow rates. I scale the calculated value so the minimum value output by the PID controller is my min PWM value and the same with the Max. Currently I have the same min and max for Heating and DHW but it might be worth splitting this out as the flow rates for a given pump duty are quite different.
  • My current deltaT comes from Home Assistant. HA is subscribed to the MQTT topic for our OSM supplied Heat Meter. It might be worth me setting up the ESP to get this directly for resilience.
  • operation_mode also comes from HA and is the operation mode the FTC outputs so 1 is DHW. For this I use a different set of PID values and assume if it isn’t 1 that the Space Heating values should be used.
  • For some reason it looks like I reset the PID Controller’s integral value when the Delta T drops below 0.4K. Not sure why I have done this, I think I was just trying things will probably remove that.
  • It might be worth me looking again at when it would be important to reset this value. I guess when starting hot water mode with new parameters I should reset it? @Rachel do you have any thoughts on this?
  • The controller averages its output over 3 samples. This is to reduce noise but I might try removing it again.
  • My current PID parameters are different to the defaults shown as I have set them in HA. These reset to the defaults when the ESP32 is restarted so it is important to update the defaults when values are found that work.
  • Current values:
    • Heating:
      • P:0.09
      • I: 0.001
      • D: 0.001
    • DHW:
      • P: 0.1
      • I: 0
      • D: 0
1 Like