patrickcollins12/esphome-fan-controller

This is so cool! But...couple of questions?

Opened this issue · 19 comments

Hi,

I have this running to control three fans on my inverter. Soon I will expand to cover a second inverter with a second set of fans (once I move out of breadboard stage). You made it so super easy to get up and running!

Question 1:
The main issue with inverter cooling is that I need a responsive cooling system but one that is skewed - I need a fast(ish) response and the long slow tail....is this where kd comes in?

Question 2:
I have a DHT dangling over the back of the inverter radiator to read the air temperature, but really I need something better to read the radiator fin temperature directly, or a way of pushing the inverter internal temperature (which HA knows) to the fan controller...but with the option to still use DHT incase HA fails for some reason. That is, use inverter internal temp reading and if null revert to DHT temperature. Is this possible? if so how the heck could I pull that off?

Question 3:
I'd really like to bring in the tachometer data so that I can detect fan failure. I can do all the logic in HA/NodeRed, just need to plumb up the pins and get a reading....any tips? I've seen a few ways of doing it with resistors, logic level shifters, capacitors etc....but it seems a little complicated? From the looks of things I have exactly the same fans as you... :-)

Thanks heaps!

CP.

Lolin D32, 2 Groups witch 2 Arctic P12 PWM PST each, 2 Dallas DS18B20.
for the PWM signal a 3.3 to 5V level shifter, it doesn't work without.

Q3:
The speed can also be read directly with an ESP. i use the simple attached circuit for this, but with one change. my Lolin D32 has internal pullups, so the external 10k resistor is not needed.
for your board you have to check yourself which GPIO have a pullup or use the external resistor.

sensor:
# RpM Fans outward
  - platform: pulse_counter
    pin: 
      number: GPIO13
      mode: INPUT_PULLUP  # activate internal pullup
    name: ${nodename}" RPM outward"
    unit_of_measurement: "UpM"
    accuracy_decimals: 0
    id: ${nodename}_rpm_outward
    update_interval: 5s
    filters:
      - multiply: 0.5    # 2 pulse per rotation
# RpM Fans inward
  - platform: pulse_counter
    pin: 
      number: GPIO27
      mode: INPUT_PULLUP
    name: ${nodename}" RPM inward
    unit_of_measurement: "UpM"
    accuracy_decimals: 0
    id: ${nodename}_rpm_inward
    update_interval: 5s
    filters:
      - multiply: 0.5

Q2:
you should get the temperature from HA with the API to ESP, but I haven't dealt with that yet.
https://esphome.io/components/api.html
https://esphome.io/components/sensor/homeassistant.html
but the problem is then when the connection to HA is lost

in your case, i would probably rather replace the DHT with a Dalls DS18b20, which I would screw directly to the heatsink.
maybe with something like this: https://aixontec.com/Cable-fastening-clamp-according-to-DIN-72573-two-layer-galvanized-steel-5-mm-10PACK

for a slightly better reading, the sensor is also available without stainless steel housing.

the control runs through the ESP and with Dallas temperature sensor and HA only gives a warning when the temperature exceeds a certain value.
maybe also add a buzzer to the ESP and give an alarm in case of an error.
https://esphome.io/components/rtttl.html

Q1:
i don't know.

3a303dd0c4262dd0aab8fea3ac53fcafa0aaf8bc
dallas-18b20-1wire-pro-temp-sensor-2-meter (1)

Most excellent! I have now hot both inverters with three fans each running. :-)

I have the DHT still in place as a backup but the cooling logic is running off the internal inverter temperature for now...I will look for a place to mount a thermocouple on the inverter but from what I can see without taking if off the wall there's not too many easy options...dangling it in space as with the DHT will not be as effective.

RPMs worked a treat too! A fun little project.

At present using KP of 0.1, KD and KI are both zero. Inverter heat is a bit chaotic as sun condition can cause wild fluctuations in generation and therefore heat output. So KP does a simple job...may add a little KD atcsomecstagecwhen I have more data. Initial testing shows the fans will knock 20-30°C off the running temperature.

Fans:
20230128_210535-edit-20230128211613

Controller:
20230128_145836

The spaghetti:
20230128_142403

HA (not running as night):
Screenshot_20230128_211016_Home Assistant

Thanks for your help and inspiration! Got me well and truly underway...

CP.

can you show the code on how to get the temperature from the HA to the ESP?

Sure thing! Code below. :-)

I do have a small section to define the "effective temperature" which is basically a 'best-of' the inverter internal or DHT temperature....that part needs a little more work when I get some more time. The external air temperature lags the internal temperature and is much lower - so I will need to fudge that (e.g. air temp + 20C to 'estimate' internal temperature) as I cannot drive the inverter set point temperature (based on internal temp) against the air temperature as the air is so much cooler. Either that or I need to create a second 'mode' - cool against internal temp, OR, cool against air temp. As it is just a backup in case HA goes away and I lose the internal temp data the first approach (+20C) seems easier.....but I've parked it for now until the weekend as I have some other things to do. :-)

substitutions:
  devicename: esp32-inverter-cooling
  friendly_name: Inverter Fan Control
  
esphome:
  name: $devicename

globals:
  - id: dhttemp
    type: float
    restore_value: yes
    initial_value: '0'
 
#########################
# ESP32 AND NETWORK SETUP

 
esp32:
  board: esp32dev
  framework:
    type: arduino

# pid climate log update is noisy, dial it back to warn
logger:
  level: DEBUG
  logs: 
    climate: ERROR
    dht: WARN

# default HA integration, OTA updater and backup http web portal
api:
ota:
captive_portal:

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  
  manual_ip:
    static_ip: 192.168.20.92
    gateway: 192.168.20.1
    subnet: 255.255.255.0
    dns1: 192.168.1.6


number:

  ## RECEIVE kp,ki and kd parameters from input_text.kx helpers in 
  # Home Assistant. See the PID controller below
  # These helper values will get saved to flash thus permanently over-riding 
  # the initial values set in the PID below.

  #Inverter PID Values
  # KP
  - platform: template
    name: $friendly_name KP
    icon: mdi:chart-bell-curve
    restore_value: true
    min_value: 0
    max_value: 50
    step: 0.001
    set_action: 
      lambda: |- 
        // ESP_LOGI("main", "!!!!!! kp from boot %d", id("n_w_inverter_fan_kp") );
        // id(n_w_inverter_thermostat).set_kp( id("$friendly_name Kp") );
        id(n_w_inverter_thermostat).set_kp( x );
        // ESP_LOGI("main", "!!!!!! kp from boot %d", id("n_e_inverter_fan_kp") );
        // id(n_e_inverter_thermostat).set_kp( id("$friendly_name Kp") );
        id(n_e_inverter_thermostat).set_kp( x );

  # KI
  - platform: template
    name: $friendly_name KI
    icon: mdi:chart-bell-curve
    restore_value: true
    min_value: 0
    max_value: 50
    step: 0.0001
    set_action: 
      lambda: id(n_w_inverter_thermostat).set_ki( x );
              id(n_e_inverter_thermostat).set_ki( x );

  # KD
  - platform: template
    name: $friendly_name KD
    icon: mdi:chart-bell-curve
    restore_value: true
    min_value: -50
    max_value: 50
    step: 0.001
    set_action: 
      lambda: id(n_w_inverter_thermostat).set_kd( x );
              id(n_e_inverter_thermostat).set_kd( x );


text_sensor:

  # Send IP Address
  - platform: wifi_info
    ip_address:
      name: $friendly_name IP Address

  # Send Uptime in raw seconds
  - platform: template
    name: $friendly_name Uptime
    id: uptime_human
    icon: mdi:clock-start


sensor:

  # Send WiFi signal strength & uptime to HA
  - platform: wifi_signal
    name: $friendly_name WiFi Strength
    update_interval: 60s

  # Human readable uptime string
  - platform: uptime
    name: $friendly_name Uptime
    id: uptime_sensor
    update_interval: 60s
    on_raw_value:
      then:
        - text_sensor.template.publish:
            id: uptime_human
            # Custom C++ code to generate the result
            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();

  # Pull in temperatures from HA
  - platform: homeassistant
    name: "N/W Inverter Internal Temperature"
    id: n_w_inverter_internal_temperature
    entity_id: sensor.solis_n_w_inverter_temperature

  - platform: homeassistant    
    name: "N/E Inverter Internal Temperature"
    id: n_e_inverter_internal_temperature
    entity_id: sensor.solis_n_e_inverter_temperature
    
  #Pull in DC power from HA
  - platform: homeassistant
    name: "N/W Inverter DC Power"
    id: n_w_inverter_dc_power
    entity_id: sensor.solis_n_w_inverter_dc_power
  
  - platform: homeassistant
    name: "N/E Inverter DC Power"
    id: n_e_inverter_dc_power
    entity_id: sensor.solis_n_e_inverter_dc_power  
    

#######################################################################################################
# Fan Monitoring

  - platform: pulse_counter
    pin: 
      number: 18
      mode: INPUT_PULLUP
    id: nw_fan_1_speed
    name: "NW Fan 1 Speed"
    update_interval: 5s
    filters:
      - multiply: 0.5
    accuracy_decimals: 0
    
  - platform: pulse_counter
    pin: 
      number: 19
      mode: INPUT_PULLUP
    id: nw_fan_2_speed
    name: "NW Fan 2 Speed"
    update_interval: 5s
    filters:
      - multiply: 0.5
    accuracy_decimals: 0
    
  - platform: pulse_counter
    pin: 
      number: 21
      mode: INPUT_PULLUP
    id: nw_fan_3_speed
    name: "NW Fan 3 Speed"
    update_interval: 5s
    filters:
      - multiply: 0.5
    accuracy_decimals: 0

  - platform: pulse_counter
    pin: 
      number: 22
      mode: INPUT_PULLUP
    id: ne_fan_1_speed
    name: "NE Fan 1 Speed"
    update_interval: 5s
    filters:
      - multiply: 0.5
    accuracy_decimals: 0
    
  - platform: pulse_counter
    pin: 
      number: 23
      mode: INPUT_PULLUP
    id: ne_fan_2_speed
    name: "NE Fan 2 Speed"
    update_interval: 5s
    filters:
      - multiply: 0.5
    accuracy_decimals: 0
    
  - platform: pulse_counter
    pin: 
      number: 25
      mode: INPUT_PULLUP
    id: ne_fan_3_speed
    name: "NE Fan 3 Speed"
    update_interval: 5s
    filters:
      - multiply: 0.5
    accuracy_decimals: 0    

    
#######################################################################################################
# FAN CONTROLLER SETUP

  # N/W Inverter
  - platform: template
    name: $friendly_name N/W p term
    id: pid_nw_p_term
    unit_of_measurement: "%"
    accuracy_decimals: 2

  - platform: template
    name: $friendly_name N/W i term
    id: pid_nw_i_term
    unit_of_measurement: "%"
    accuracy_decimals: 2

  - platform: template
    name: $friendly_name N/W d term
    id: pid_nw_d_term
    unit_of_measurement: "%"
    accuracy_decimals: 2

  - platform: template
    name: $friendly_name N/W output value
    unit_of_measurement: "%"
    id: pid_nw_o_term
    accuracy_decimals: 2

  - platform: template
    name: $friendly_name N/W error value
    id: pid_nw_e_term
    accuracy_decimals: 2

  - platform: template
    name: $friendly_name N/W zero 
    id: pid_nw_zero_value
    update_interval: 60s
    lambda: |-
      return 0;

  - platform: template
    name: $friendly_name N/W zero percent
    unit_of_measurement: "%"
    id: pid_nw_zero_value_percent
    update_interval: 60s
    lambda: |-
      return 0;
      
   # N/E Inverter
  - platform: template
    name: $friendly_name N/E p term
    id: pid_ne_p_term
    unit_of_measurement: "%"
    accuracy_decimals: 2

  - platform: template
    name: $friendly_name N/E i term
    id: pid_ne_i_term
    unit_of_measurement: "%"
    accuracy_decimals: 2

  - platform: template
    name: $friendly_name N/E d term
    id: pid_ne_d_term
    unit_of_measurement: "%"
    accuracy_decimals: 2

  - platform: template
    name: $friendly_name N/E output value
    unit_of_measurement: "%"
    id: pid_ne_o_term
    accuracy_decimals: 2

  - platform: template
    name: $friendly_name N/E error value
    id: pid_ne_e_term
    accuracy_decimals: 2

  - platform: template
    name: $friendly_name N/E zero 
    id: pid_ne_zero_value
    update_interval: 60s
    lambda: |-
      return 0;

  - platform: template
    name: $friendly_name N/E zero percent
    unit_of_measurement: "%"
    id: pid_ne_zero_value_percent
    update_interval: 60s
    lambda: |-
      return 0;
      
#######################################################################################################      

  # GET TEMP/HUMIDITY FROM DHT22 --> N/W
  - platform: dht
    pin: GPIO13
    temperature:
      name: "N/W Inverter Temperature (DHT)"
      id: n_w_inverter_dht_temperature
      accuracy_decimals: 1

      #Smooth the readings to stop jumpy control
      filters:
        - exponential_moving_average:  
            alpha: 0.1
            send_every: 5

    update_interval: 1.5s
  
   # GET TEMP/HUMIDITY FROM DHT22 --> N/E
  - platform: dht
    pin: GPIO14
    temperature:
      name: "N/E Inverter Temperature (DHT)"
      id: n_e_inverter_dht_temperature
      accuracy_decimals: 1

      #Smooth the readings to stop jumpy control
      filters:
        - exponential_moving_average:  
            alpha: 0.1
            send_every: 5

    update_interval: 1.5s

#######################################################################################################
    
  # Set the temperature value to use (internal or DHT temperature)
  # N/W Inverter
  - platform: template
    name: "N/W Inverter Temperature (Effective)"
    id: n_w_inverter_temperature_effective
    lambda: 'return (id(n_w_inverter_internal_temperature).has_state() ? id(n_w_inverter_internal_temperature).state : id(n_w_inverter_dht_temperature).state);'
    update_interval: 60s
  
  # N/E Inverter
  - platform: template
    name: "N/E Inverter Temperature (Effective)"
    id: n_e_inverter_temperature_effective
    lambda: 'return (id(n_e_inverter_internal_temperature).has_state() ? id(n_e_inverter_internal_temperature).state : id(n_e_inverter_dht_temperature).state);'
    update_interval: 60s
  
#######################################################################################################  
  
  # Take the "COOL" value of the pid and send 
  # it to the frontend to graph the output voltage
  - platform: pid
    name: "N/W Inverter Fan Speed (PWM_V)"
    climate_id: n_w_inverter_thermostat
    accuracy_decimals: 0
    type: COOL
    
  - platform: pid
    name: "N/E Inverter Fan Speed (PWM_V)"
    climate_id: n_e_inverter_thermostat
    accuracy_decimals: 0
    type: COOL

#######################################################################################################
output:
  # Wire this pin into the PWM pin the 12v fan
  # ledc is the name of the pwm output system on an esp32
  
  # N/W Inverter
  - platform: ledc
    id: n_w_inverter_fan_speed
    pin: 16

    # 25KHz is standard PC fan frequency, minimises buzzing
    frequency: "25000 Hz" 

    # The fans stop working below 12% signal (at 13% one will still try).
    min_power: 11%
    max_power: 100%

  # N/E Inverter
  - platform: ledc
    id: n_e_inverter_fan_speed
    pin: 17

    # 25KHz is standard PC fan frequency, minimises buzzing
    frequency: "25000 Hz" 

    # The fans stop working below 13% signal.
    min_power: 13%
    max_power: 100%

#######################################################################################################
# Expose a PID-controlled Thermostat
# Manual: https://esphome.io/components/climate/pid.html

climate:
  
  # N/W Inverter
  - platform: pid
    name: "N/W Inverter Thermostat"
    id: n_w_inverter_thermostat
    sensor: n_w_inverter_temperature_effective

    # 30c is a decent target as the inverter will be about 40C on the inside at that point.
    default_target_temperature: 30°C
    cool_output: n_w_inverter_fan_speed

    on_state:
      - sensor.template.publish:
          id: pid_nw_p_term
          state: !lambda 'return -id(n_w_inverter_thermostat).get_proportional_term() * 100.0;'
      - sensor.template.publish:
          id: pid_nw_i_term
          state: !lambda 'return -id(n_w_inverter_thermostat).get_integral_term()* 100.0;'
      - sensor.template.publish:
          id: pid_nw_d_term
          state: !lambda 'return -id(n_w_inverter_thermostat).get_derivative_term()* 100.0;'
      - sensor.template.publish:
          id: pid_nw_o_term
          state: !lambda 'return -id(n_w_inverter_thermostat).get_output_value()* 100.0;'
      - sensor.template.publish:
          id: pid_nw_e_term
          state: !lambda 'return -id(n_w_inverter_thermostat).get_error_value();'
        
   
    # The extents of the HA Thermostat
    visual:
      min_temperature: 20 °C
      max_temperature: 70 °C
  
    # See the README for setting up these parameters.
    # These are over ridden by the number templates above.
    control_parameters:
      kp: 0.1
      ki: 0.0
      kd: 0.0
      max_integral: 0.0

#----------------------------------------------------------------------------------------------------

 # N/E Inverter
  - platform: pid
    name: "N/E Inverter Thermostat"
    id: n_e_inverter_thermostat
    sensor: n_e_inverter_temperature_effective

    # 30c is a decent target as the inverter will be about 40C on the inside at that point.
    default_target_temperature: 30°C
    cool_output: n_e_inverter_fan_speed

    on_state:
      - sensor.template.publish:
          id: pid_ne_p_term
          state: !lambda 'return -id(n_e_inverter_thermostat).get_proportional_term() * 100.0;'
      - sensor.template.publish:
          id: pid_ne_i_term
          state: !lambda 'return -id(n_e_inverter_thermostat).get_integral_term()* 100.0;'
      - sensor.template.publish:
          id: pid_ne_d_term
          state: !lambda 'return -id(n_e_inverter_thermostat).get_derivative_term()* 100.0;'
      - sensor.template.publish:
          id: pid_ne_o_term
          state: !lambda 'return -id(n_e_inverter_thermostat).get_output_value()* 100.0;'
      - sensor.template.publish:
          id: pid_ne_e_term
          state: !lambda 'return -id(n_e_inverter_thermostat).get_error_value();'
        
   
    # The extents of the HA Thermostat
    visual:
      min_temperature: 20 °C
      max_temperature: 70 °C
  
    # See the README for setting up these parameters.
    # These are over ridden by the number templates above.
    control_parameters:
      kp: 0.1
      ki: 0.0
      kd: 0.0
      max_integral: 0.0

#######################################################################################################
# Manual Fan Control.

fan:
  # N/W Fans
  - platform: speed
    output: n_w_inverter_fan_speed
    name: "N/W Inverter Fan Speed"

  # N/E Fans
  - platform: speed
    output: n_e_inverter_fan_speed
    name: "N/E Inverter Fan Speed"


#######################################################################################################
# Expose an ESP32 restart button to HA
switch:
  - platform: restart
    name: "Inverter Cooling ESP32 Restart"

a suggestion.

the temperature at the DHT is surely lower than the internal temperature. So if it waits for 30 degrees at the DHT, the internal temperature could already be too high. Maybe add an offset to the DHT when selecting the effective temperature.

I have just looked at the data (boring work meeting) and have added "offset: 15" to the DHT configuration....seems to be working nicely and reasonably aligned with he internal temperature. ;-)

why can't you put the sensor inside the inverter? Why does the inverter not have its own cooling? what is the motivation behind / problem this project?

Question 1: - The main issue with inverter cooling is that I need a responsive cooling system but one that is skewed - I need a fast(ish) response and the long slow tail....is this where kd comes in?

Crank up the KP it'll respond quickly, but it might oscillate the power up and down until it finds equilibrium. Try PID Autotune, it might work for you if it is a fast response system.

Question 2:
I have a DHT dangling over the back of the inverter radiator to read the air temperature, but really I need something better to read the radiator fin temperature directly, or a way of pushing the inverter internal temperature (which HA knows) to the fan controller...but with the option to still use DHT incase HA fails for some reason. That is, use inverter internal temp reading and if null revert to DHT temperature. Is this possible? if so how the heck could I pull that off?

Attach the DHT directly to the fin? I wouldn't try to get the reading from HA into your ESP32 because then your cooling is dependent on all of the systems being connected together. But if you do want to do that, just expose a number template to HA and then with some HA automation you could one value to the other.

Question 3:
I'd really like to bring in the tachometer data so that I can detect fan failure. I can do all the logic in HA/NodeRed, just need to plumb up the pins and get a reading....any tips? I've seen a few ways of doing it with resistors, logic level shifters, capacitors etc....but it seems a little complicated? From the looks of things I have exactly the same fans as you... :-)

I like @DunklesKaltesNichts 's suggestion, just wire the tach directly to another pin. I'd be keen to hear how you get on with it.

BTW those fan mounts like amazing. well done. space age.

So the inverter has a temperature sensor and I use MODBUS to read this out and give it to HA. This i can pass back to the esp32 so it can run to this temperature as it's the best data I can get from the heart of the machine. The inverter doesn't have it's own active cooling system - it has a passive radiator/ heatsink on the back (between the inverter and the wall) and it relies on passive air movement and the draft that's created as the hot air rises. During peak production the temperature of the inverter went to over 70C which is bad for efficiency and not good for the life of the machine. So adding some fans to draw air up and out makes the heatsink super effective - the max temperature now is about 42C....30C less. :-) I can't put a probe inside the inverter (void warranty, plus it's all solid state electronics so don't want to mess with it) and there's not really much room to neatly attach a probe to the heat sink either....but I can dangle a DHT over the heatsink and measure the air temperature from the rising warm air. There's a max of about a 15-18C temperature difference between core temperature and air temperature when the fan is running. I want this because if HA goes away (dead, crashed while I'm on holiday, being rebuilt, etc) I still want the controlled cooling - there'd be no HA-based data feed of the internal temperature, but I can still get the DHT temperature locally and I can run and control the fans with that... Meaning the fans will stop when it's cool and work harder when it's hot (versus not running at all or running at full speed). I use the fan rpm data (or will when I get there) to detect fan or 12v PSU failure, and if my ESP32 or 5v dies the fans will run at 100% (no PWM) but HA can tell me it's gone offline (or, again, it will when I get there). Make sense?

On Tue, 31 Jan 2023, 16:33 Patrick Collins, @.> wrote: why can't you put the sensor inside the inverter? Why does the inverter not have its own cooling? what is the motivation behind / problem this project? — Reply to this email directly, view it on GitHub <#10 (comment)>, or unsubscribe https://github.com/notifications/unsubscribe-auth/AUIP7ZXQCZ5BKEWSMZVHPCTWVCBXNANCNFSM6AAAAAAUFUNCV4 . You are receiving this because you authored the thread.Message ID: @.>

Yep, makes sense! You definitely want this running independently of HA. Checkout the new deadband features I added to PID Climate.

So the inverter has a temperature sensor and I use MODBUS to read this out and give it to HA

esphome can also read modbus. do i read my solar charge controllers and send the data to HA.

https://esphome.io/components/modbus_controller.html

tere is an example of this in the cookbook.

https://esphome.io/cookbook/tracer-an.html

So I did actually try to apply some dead band config to my YAML (based on what was here: https://esphome.io/components/climate/pid.html) but I couldn't get the config to save and deploy - it kept telling me that the config wasn't valid (e.g. 'dead band' is not a valid tag in this section). I wanted to do this to flatten the fan response so it ramps up quickly and but doesn't back off so fast by making a lopsided dead band range. But had to give up as it just wouldn't work. Have you got an example?

I know that ESPHome can do some MODBUS, but my inverter will only permit one connection at a time - so I've kicked it off the Internet by applying a blocking rule on the FW to that there is no "phone home" connection, and now I talk to it via HA....and all the data I get from that is useful so the loopback of the temperature back to the ESP is still the easiest. The DHT-based temperature (fudged or otherwise) isn't ideal but it's only a backup....and sometimes near enough is good enough....

Can you post the YAML you were using to try and implement deadband?

So I copied the example from the link above:

# Example configuration entry
climate:
  - platform: pid
    name: "PID Climate Controller"
    sensor: temperature_sensor
    default_target_temperature: 21°C
    heat_output: heater
    control_parameters:
      kp: 0.49460
      ki: 0.00487
      kd: 12.56301
      output_averaging_samples: 5      # smooth the output over 5 samples
      derivative_averaging_samples: 5  # smooth the derivative value over 10 samples
    deadband_parameters:
      threshold_high: 0.5°C       # deadband within +/-0.5°C of target_temperature
      threshold_low: -0.5°C

Making my section look like this:

climate:
  
  # N/W Inverter
  - platform: pid
    name: "N/W Inverter Thermostat"
    id: n_w_inverter_thermostat
    sensor: n_w_inverter_temperature_effective

    # 35C is a decent target!
    default_target_temperature: 35°C
    cool_output: n_w_inverter_fan_speed

    on_state:
      - sensor.template.publish:
          id: pid_nw_p_term
          state: !lambda 'return -id(n_w_inverter_thermostat).get_proportional_term() * 100.0;'
      - sensor.template.publish:
          id: pid_nw_i_term
          state: !lambda 'return -id(n_w_inverter_thermostat).get_integral_term()* 100.0;'
      - sensor.template.publish:
          id: pid_nw_d_term
          state: !lambda 'return -id(n_w_inverter_thermostat).get_derivative_term()* 100.0;'
      - sensor.template.publish:
          id: pid_nw_o_term
          state: !lambda 'return -id(n_w_inverter_thermostat).get_output_value()* 100.0;'
      - sensor.template.publish:
          id: pid_nw_e_term
          state: !lambda 'return -id(n_w_inverter_thermostat).get_error_value();'
        
   
    # The extents of the HA Thermostat
    visual:
      min_temperature: 25 °C
      max_temperature: 50 °C
  
    # See the README for setting up these parameters.
    # These are over ridden by the number templates above.
    control_parameters:
      kp: 0.1
      ki: 0.0
      kd: 0.0
      max_integral: 0.0
      
    deadband_parameters:
      threshold_high: 0.5°C       # deadband within +/-0.5°C of target_temperature
      threshold_low: -1.0°C

It gives the error:

[deadband_parameters] is an invalid option for [climate.pid]. Did you mean [control_parameters]?

But if I move under control_parameters it says:

[deadband_parameters] is an invalid option for [control_parameters]. Please check the indentation.

So I got nowhere fast....I try re-organising the block by moving the on_state: section to the bottom of the pid definition etc etc but all I go was errors. I simply couldn't find a way to swallow the config..... sometime I could get past the editor error, but then the compile would fail...so something somewhere isn't quite right....

Aha, you don't have the latest version of esphome.
pip3 install -U esphome