PID Controller for Air Supply with 30%+ Energy Savings

Hello, community! I would like to share my solution, which significantly reduced energy consumption (by 30-40%, according to my calculations) when using a duct heater (2.5 kW) in a DHW system with a recuperator. Instead of a primitive On/Off thermostat, I developed an autonomous PID controller based on the ESP32, which: Provides a stable target temperature (5°C) with an accuracy of $\pm 0.3^\circ C$ at the recuperator inlet, regardless of the frost outside (down to -20°C). Smooth power control (via a dimmer on the SSR) eliminates temperature surges and increases comfort in the home. Includes a strict safety system (only turns on when the fan is running) to prevent overheating. This is a complete, ready-to-use device, integrated with Home Assistant for monitoring and control.

# =================================================================
# --- 1. Основная конфигурация устройства (ПЛАТА И СЕТЬ) ---
# =================================================================
esphome:
  name: nagrev-pritoka-20
  friendly_name: nagrev_pritoka_2.0

esp32:
  board: esp32dev
  framework:
    type: arduino

logger:

api:
  encryption:
    key: "06RAujzMZGvHtNURcFPc7zJxjfgM4npzwkqcF/6OR7Y="
  reboot_timeout: 5min



ota:
  - platform: esphome
    password: "de5857967d95527f7dab10acaf66f437"

wifi:
  ssid: 
  password: 
  manual_ip:
    static_ip: 
    gateway: 
    subnet: 
  ap:
    ssid: "Nagrev-Pritoka-13"
    password: "jLRjYrAoNgOg"

captive_portal:

web_server:
  port: 80

# --- Конфигурация интерфейсов ---
uart:
  tx_pin: GPIO17
  rx_pin: GPIO16
  baud_rate: 9600

one_wire:
  - platform: gpio
    pin: GPIO13

# =================================================================
# --- 2. SENSORS (Датчики: Dallas Temp & PZEM-004T) ---
# =================================================================
sensor:
  - platform: dallas_temp
    address: 0xb9020491770ed528
    name: "Улица/перед нагревателем"
    id: outdoor_temperature
    update_interval: 55s

  - platform: dallas_temp
    address: 0xd9000000b3d1bd28
    name: "Темп после нагревателя"
    id: heater_temperature
    filters:
      # Защитные фильтры оставлены, чтобы убрать невалидные показания
      - timeout: 30s
      - filter_out: -127.0
      - filter_out: 85.0
    update_interval: 5s

  - platform: dallas_temp
    address: 0xa3020891777a5f28
    name: "Темп после рекупа"
    id: temperature_posle_rekupa
    update_interval: 65s

  - platform: dallas_temp
    address: 0xf5000000b3b49e28
    name: "Вытяжка перед рекупом"
    id: vytazhka_temp_pered_rekupom
    update_interval: 2min

  - platform: dallas_temp
    address: 0xfb0204917729d028
    name: "Вытяжка после рекупа"
    id: vytazhka_temp_posle_rekupa
    update_interval: 90s

  - platform: pzemac
    address: 5
    current: 
      name: "Канальный нагреватель амперы"
      id: load_current
    power:
      name: "Канальный нагреватель нагрузка"
    power_factor:
      name: "Канальный нагреватель Power Factor"
    update_interval: 4s

  - platform: wifi_signal
    name: "WiFi Signal Nagrev"
    update_interval: 60s

# =================================================================
# --- 3. OUTPUTS & SWITCHES (Диммер и Реле) ---
# =================================================================
output:
  - platform: ac_dimmer
    id: ac_dimmer_physical
    gate_pin: GPIO27
    zero_cross_pin:
      number: GPIO14
      mode:
        input: true
        pullup: true
      inverted: no
    min_power: 0.1

switch:
  - platform: gpio
    pin: GPIO26
    id: master_relay_switch
    name: "Мастер-реле нагревателя"
    restore_mode: ALWAYS_OFF
    # 🔴 ИСПРАВЛЕНИЕ: Делаем реле внутренним для предотвращения ручного управления
    # internal: true    

  # --- Переключатель для АВТОМАТИЧЕСКОГО ОБОГРЕВА ---
  - platform: template
    id: auto_mode_switch
    name: "Включить/Выключить Автоматический Обогрев"
    lambda: 'return id(pid_heater).mode == climate::CLIMATE_MODE_HEAT;'
    turn_on_action:
      - if:
          condition:
            # Разрешаем включение, только если главный блокиратор активен
            binary_sensor.is_on: heating_allowed
          then:
            - switch.turn_on: master_relay_switch # Включаем физическое питание
            - climate.control: { id: pid_heater, mode: 'HEAT' }
    turn_off_action:
      - switch.turn_off: master_relay_switch # Выключаем физическое питание
      - climate.control: { id: pid_heater, mode: 'OFF' }

# =================================================================
# --- 4. Виртуальный Сенсор (Home Assistant Fan Status) ---
# =================================================================
binary_sensor:
  # --- 4.1. Сенсор состояния вентилятора ---
  - platform: homeassistant
    id: virtual_indicator 
    name: "Виртуальный Сенсор-Индикатор"
    # !!! ВАЖНО: ЗАМЕНИТЕ entity_id НА ID ВАШЕГО ВЕНТИЛЯТОРА В HOME ASSISTANT !!!
    entity_id: light.pritochka2025_sonoff_d1_dimmer 
    # Этот сенсор будет ON, только если Home Assistant сообщает "on". 
    # В любом другом случае (off, unknown, unavailable) он будет OFF.

  - platform: template
    name: "Статус Вентилятора Притока (HA)" # Имя вашего целевого виртуального сенсора
    id: virtual_indicator2
    lambda: |-
      // Возвращает TRUE (ВКЛ) или FALSE (ВЫКЛ) в зависимости от состояния вентилятора в HA
      return id(virtual_indicator).state;

  # --- 4.2. Сенсор логики (T_улица < T_уставка - 0.3) ---
  - platform: template
    id: outdoor_temp_too_low
    name: "T Улица Ниже Уставки (Требуется Нагрев)"
    lambda: 'return id(outdoor_temperature).state < id(pid_heater).target_temperature - 0.3;'


  # --- 4.3. Сезонный блокиратор (Т улицы ниже 10C) ---
  - platform: template
    id: seasonal_blocking
    name: "Сезонный Блокиратор (Т улица < 10C)"
    # Нагрев нужен только если на улице температура ниже 10°C (порог, когда рекуператор не справляется)
    lambda: 'return id(outdoor_temperature).state < 10.0;'

  # --- 4.4. ГЛАВНЫЙ СЕНСОР АКТИВАЦИИ (Вентилятор И Холод) ---
  - platform: template
    id: heating_allowed
    name: "Разрешение на Обогрев (Финальный Блокиратор)"
    # Сенсор включен только если ОБА сенсора ON (Вентилятор ON И Улица Холодно ON)
    lambda:  'return id(virtual_indicator).state && id(outdoor_temp_too_low).state && id(seasonal_blocking).state;'
    # 🔴 КРИТИЧЕСКАЯ ЗАЩИТА: Мгновенное отключение при блокировке
    on_state:
      - if:
          condition:
            # Если сенсор перешел в состояние OFF (т.е. вентилятор или температура блокируют нагрев)
            not:
              binary_sensor.is_on: heating_allowed
          then:
            - switch.turn_off: master_relay_switch # Обесточить нагреватель
            - climate.control: { id: pid_heater, mode: 'OFF' }

    # 🟢 РЕЖИМ ОЖИДАНИЯ: Запускает систему, как только разрешено, если тумблер 'auto_mode_switch' включен
      - if:
            condition:
              # Проверяем, что пользователь ранее включил главный тумблер
              switch.is_on: auto_mode_switch
            then:
              - switch.turn_on: master_relay_switch # ➡️ Включаем физическое питание
              - climate.control: { id: pid_heater, mode: 'HEAT' } # ➡️ Запускаем PID

# ----------------- CLIMATE CONTROL (PID) -----------------

climate:
  - platform: pid
    visual:
      min_temperature: 0
      max_temperature: 20 # Установлен лимит 20°C по запросу
      temperature_step: 0.1  
    id: pid_heater
    name: "PID Нагреватель"
    sensor: heater_temperature
    default_target_temperature: 5.0
    heat_output: ac_dimmer_physical
    control_parameters:
      kp: 0.16 
      ki: 0.001 
      kd: 0.0 
      output_averaging_samples: 5 # Сглаживание выхода диммера
      derivative_averaging_samples: 10 # Сглаживание входа Kd
    deadband_parameters:
      threshold_high: 0.1°C 
      threshold_low: -0.1°C