Panel rekuperatora w Home Assistant

Sterownik rekuperatora cwk-450 o3zone/Paruszewski

Wybór centrali został dokonany dawno temu, głównie ze względu na jej “polskość” i dużą otwartość autora naPanel rekuperatora w Home Assistant YouTube.  Teraz (połowa 2026r.) przyszła pora na jej zamówienie i instalacje. W przeciągu dwóch lat sporo się zmieniło i pojawiły się modele z EPP (polipropylen spieniony) oraz stali kwasoodpornej. Wybór padł na EPP ECO (nie mam pojęcia co dopisek ECO oznacza). Cena tez znacząco wzrosła – planowałem 6k zł, wyszło 9k zł.

Celem niniejszego wpisu nie jest dobór/wybór centrali, jedynie udokumentowanie jak ją podłączyć i użytkować w sposób “opensource” z Home Assistant.

Sterowanie – teoria

Przeglądając dokumentacje, oglądając zdjęcia, filmiki i dopasowując elementy wyszło na to, że rekuperatory używają sterowników Compit Aero, model zależy od samej centrali i wentylatorów które używa (i zapewne funkcji dodatkowych). Tańsze modele, mające wentylatory AC (to tylko taka nazwa – wszystkie są 230V AC, ale niektóre maja sterowanie DC i modulowanie obrotów wbudowane w wentylator, a niektóre nie maja sterowania i trzeba bezpośrednio zmniejszać napięcie lub okres AC – nie wiem jak to sterowanie jest w tym wypadku realizowane, nie interesowałem się tym) używają modelu Aero 2B, pozostałe Aero 4A +.
Sterowniki te maja możliwość komunikacji za pomocą portu RS485, jednak producent opisuje go jako prywatny protokół i nie udostępnia dokumentacji. Wprawdzie, można zaleźć opis części komend do innych sterowników Compita i zapewne sam protokół był by w jakimś stopniu zgodny, ale dalej jest potencjalnie spore ograniczenie i droga przez mękę by używać. Aero 2B/4A sa tylko sterownikami, nie komunikuje się z użytkownikiem, do tego sa dodatkowe moduły jak panele Nano, w tym w wersji z kolorowym wyświetlaczem, który udostępnia możliwość komunikacji po protokole Modbus. Zestaw komend do Modbusa jest w jakiejś części dostępny – widziałem projekt na Githubie – ale to juz drugi interface pośredni, za który trzeba dopłacić 1k zł i kolejna porcja ograniczeń. Compit udostępnia od niedawna własną “usługę chmurowa” z dostępem zdalnym do centrali, ale to nigdy nie było dla mnie opcją braną pod uwagę – dla żadnego sprzętu w domu.

Po tym dość długim wyjaśnieniu, stwierdziłem, że prościej będzie mi zrobić sterownik samemu niż integrować sie z tym całym “bajzlem”, a do samego sterowania nie ma tak wiele, bo aż 3 elementy:

  • dwa wentylatory
  • bypass

oraz będę mógł dodać tyle funkcji ile mi się zamarzy 🙂

Aby ułatwić sobie prace (i jest to tez dużo sprawniejsze rozwiązanie), nabyłem rekuperator z wentylatorami tzw. DC, czyli sterowane napięciem 1-10V lub sygnałem PWM. Cos, z czym bez problemu sobie poradzimy przy ESP32. Wentylatory są firmy EBM Papst i modelu oznaczonym VBH0190RSLEZ (K3G190‑RD45‑09).

W moim wypadku, tez koniecznie chciałem mieć bypass do szybszej wentylacji domu (tak, na lato do “chlodzenia”), w mojej jednostce jest on przełączany siłownikiem Belimo CM230-L CMS – czyli bezpośrednio 230V. Co mnie troche zasmuciło, bypass nie jest już “jakości przemysłowej”, jak pan Paruszewszki na filmikach YT się żalił ze taki drogi a on go instaluje, tylko blaszany “home made” przez producenta reku – acz trzeba przyznać, że w późniejszych filmach jest prezentowany ten model bypassu.

Dodatkowo jednostka (co mnie trochę zdziwiło – nie widziałem tego w opisie) posiadała 4 termistory, po jednym na każdym kanale, oczywiści do pomiaru temperatury :).

Zmiany

Co fizycznie u mnie wymaga zmian… po pierwsze, muszę przenieść skrzynkę sterująca z góry jednostki na jej spód. Centrala będzie na ścianie pod samym sufitem i jej obecne umiejscowienie jet kompletnie bez sensu, zakładając, że pewnie trochę będę przy tym dłubał.

Po drugie, termistory wyleciały od razu i zastąpiłem je czujnikami SHT30 – głównie dlatego, że cyfrowy sygnał łatwiej ogarnąć i dodatkowo mam pomiar wilgotności. A, że zamówiłem od razu, wymiennik entalpiczny, fajnie móc zobaczyć czy coś zmienia.

Może na wyrost, może się nie przyda, ale na tym etapie chce też móc sterować wentylatorami na podstawie przepływu powietrza, a nie ich obrotów. Czyli aby ilość powietrza wtłaczanego do domu i wyciąganego była zbilansowana. Do tego potrzebuje mierzyć jego ilość. Wstępnie zakładam pomiar ciśnienia na zwężce Venturiego, po stronach domowych centrali. Pozwoli to zmierzyć prędkość przepływu powietrza, a znając rozmiary rur dolotowych i jego objętość.
Ma to dodatkową zaletę, ze przy włączonym wyciągu kuchennym, pozwoli tez zależnie od jego biegu, zwiększyć odpowiednio ilość wtłaczanego powietrza by nie powodować podciśnienia w domu.

Schematy

+10 V
  |
  +--- R_PC817 = 1.8 kOhm --- LED wejściowa PC817 ---+
  |                                                   |
  +--- R_LED = 3.3 kOhm do 4.7 kOhm --- LED status --+--- TACH

 

 

3.3 V ──[4.7 kOhm]── GPIO35 ── kolektor PC817 #1
                       |
                      PCNT

3.3 V ──[4.7 kOhm]── GPIO34 ── kolektor PC817 #2
                       |
                      PCNT

emitery PC817 ─────── GND ESP32

 

 

Kod ESPHome dla modułu rekuperatora.

substitutions:
  devicename: rekuperator
  friendly_name: Rekuperator
  belimo_pulse_time: 88s

  # Venturi geometry
  venturi_D_mm: "194.0"   # upstream pipe diameter (200-2*3)
  venturi_d_mm: "154.0"   # throat diameter (160-2*3)

  # Assumed absolute air pressure for density calculation
  air_pressure_pa: "101325.0"

  # Venturi zero-noise threshold [Pa]
  venturi_noise: "0.002f"

esphome:
  name: ${devicename}
  friendly_name: ${friendly_name}
  on_boot:
    priority: -100
    then:
      - delay: 500ms
      - fan.turn_off: fan1_pwm
      - fan.turn_off: fan2_pwm
  on_shutdown:
    then:
      - fan.turn_off: fan1_pwm
      - fan.turn_off: fan2_pwm
      - switch.turn_off: recuperator_main_relay

esp32:
  variant: esp32
  framework:
    type: esp-idf

logger:

api:
  encryption:
    key: ""

ota:
  - platform: esphome

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  ap:
    ssid: Rekuperator Fallback Hotspot
    password: "TestTest0000"

captive_portal:

web_server:
  port: 80

i2c:
  id: i2c_main
  sda: GPIO21
  scl: GPIO19
  scan: true

tca9548a:
  - id: mux0
    address: 0x70
    i2c_id: i2c_main
    channels:
      # CH0 -> SHT40 "Czerpnia"
      - bus_id: i2c_ch0
        channel: 0

      # CH1 -> SHT40 "Wyrzutnia"
      - bus_id: i2c_ch1
        channel: 1

      # Kanał 2 ma uszkodzony styk

      # CH3 -> SHT40 "Nawiew"
      - bus_id: i2c_ch2
        channel: 3

      # CH4 -> SHT40 "Wywiew"
      - bus_id: i2c_ch3
        channel: 4

      # CH5 -> SDP810-125Pa Venturi "Wlot"
      - bus_id: i2c_ch4
        channel: 5

      # CH6 -> SDP810-125Pa Venturi "Wylot"
      - bus_id: i2c_ch5
        channel: 6

globals:
  # Pamięć regulatora PI dla synchronizacji przepływu.
  - id: flow_sync_integral
    type: float
    restore_value: false
    initial_value: '0.0f'

  # PWM wyliczone przez regulator; -1 oznacza: nie zmieniaj wentylatora.
  - id: flow_sync_next_speed
    type: int
    restore_value: false
    initial_value: '-1'

sensor:
  # ---- SHT4x sensors ----
  - platform: sht4x
    i2c_id: i2c_ch1
    update_interval: 30s
    precision: High
    temperature:
      name: "Czerpnia - Temperatura"
      id: czerpnia_temp
    humidity:
      name: "Czerpnia - Wilgotność"
      id: czerpnia_hum

  - platform: sht4x
    i2c_id: i2c_ch3
    update_interval: 30s
    precision: High
    temperature:
      name: "Wyrzutnia - Temperatura"
      id: wyrzutnia_temp
    humidity:
      name: "Wyrzutnia - Wilgotność"
      id: wyrzutnia_hum

  - platform: sht4x
    i2c_id: i2c_ch2
    update_interval: 30s
    precision: High
    temperature:
      name: "Nawiew - Temperatura"
      id: nawiew_temp
    humidity:
      name: "Nawiew - Wilgotność"
      id: nawiew_hum

  - platform: sht4x
    i2c_id: i2c_ch0
    update_interval: 30s
    precision: High
    temperature:
      name: "Wywiew - Temperatura"
      id: wywiew_temp
    humidity:
      name: "Wywiew - Wilgotność"
      id: wywiew_hum

  # ---- SDP810-125Pa ----
  - platform: sdp3x
    i2c_id: i2c_ch4
    address: 0x25
    measurement_mode: differential_pressure
    update_interval: 2s
    name: "Wlot Venturi Differential Pressure"
    id: wlot_venturi_dp
    unit_of_measurement: "Pa"
    filters:
      # Martwa strefa: usuwa szum pomiaru, gdy wentylator stoi.
      - lambda: |-
          if (fabsf(x) < ${venturi_noise}) {
            return 0.0f;
          }
          return x;

  - platform: sdp3x
    i2c_id: i2c_ch5
    address: 0x25
    measurement_mode: differential_pressure
    update_interval: 2s
    name: "Wylot Venturi Differential Pressure"
    id: wylot_venturi_dp
    unit_of_measurement: "Pa"
    filters:
      # Martwa strefa: usuwa szum pomiaru, gdy wentylator stoi.
      - lambda: |-
          if (fabsf(x) < ${venturi_noise}) {
            return 0.0f;
          }
          return x;

  # ---- Derived air density from temperature ----
  - platform: template
    name: "Wlot - gęstość powietrza"
    id: wlot_air_density
    unit_of_measurement: "kg/m³"
    accuracy_decimals: 3
    update_interval: 30s
    lambda: |-
      const float t_c = id(nawiew_temp).state;
      if (isnan(t_c)) return NAN;

      const float T = t_c + 273.15f;
      if (T <= 0.0f) return NAN;

      return ${air_pressure_pa} / (287.05f * T);

  - platform: template
    name: "Wylot - gęstość powietrza"
    id: wylot_air_density
    unit_of_measurement: "kg/m³"
    accuracy_decimals: 3
    update_interval: 30s
    lambda: |-
      const float t_c = id(wywiew_temp).state;
      if (isnan(t_c)) return NAN;

      const float T = t_c + 273.15f;
      if (T <= 0.0f) return NAN;

      return ${air_pressure_pa} / (287.05f * T);

  # ---- Venturi velocity ----
  - platform: template
    name: "Wlot - prędkość powietrza"
    id: wlot_air_velocity
    unit_of_measurement: "m/s"
    accuracy_decimals: 2
    update_interval: 2s
    lambda: |-
      const float dp = id(wlot_venturi_dp).state;
      const float rho = id(wlot_air_density).state;

      if (isnan(dp) || isnan(rho) || dp <= 0.0f || rho <= 0.0f) {
        return 0.0f;
      }

      const float D = ${venturi_D_mm} / 1000.0f;
      const float d = ${venturi_d_mm} / 1000.0f;
      const float beta = d / D;
      const float Cd = 0.98f;
      const float A1 = 3.14159265f * D * D / 4.0f;
      const float A2 = 3.14159265f * d * d / 4.0f;
      const float denom = rho * (1.0f - powf(beta, 4.0f));

      if (denom <= 0.0f || A1 <= 0.0f) return NAN;

      const float q = Cd * A2 * sqrtf((2.0f * dp) / denom);
      return q / A1;

  - platform: template
    name: "Wylot - prędkość powietrza"
    id: wylot_air_velocity
    unit_of_measurement: "m/s"
    accuracy_decimals: 2
    update_interval: 2s
    lambda: |-
      const float dp = id(wylot_venturi_dp).state;
      const float rho = id(wylot_air_density).state;

      if (isnan(dp) || isnan(rho) || dp <= 0.0f || rho <= 0.0f) {
        return 0.0f;
      }

      const float D = ${venturi_D_mm} / 1000.0f;
      const float d = ${venturi_d_mm} / 1000.0f;
      const float beta = d / D;
      const float Cd = 0.98f;
      const float A1 = 3.14159265f * D * D / 4.0f;
      const float A2 = 3.14159265f * d * d / 4.0f;
      const float denom = rho * (1.0f - powf(beta, 4.0f));

      if (denom <= 0.0f || A1 <= 0.0f) return NAN;

      const float q = Cd * A2 * sqrtf((2.0f * dp) / denom);
      return q / A1;

  # ---- Volumetric airflow ----
  - platform: template
    name: "Wlot - przepływ powietrza"
    id: wlot_airflow
    unit_of_measurement: "m³/h"
    accuracy_decimals: 1
    update_interval: 2s
    filters:
      # Usuwa krótkie piki i pojedyncze anomalie.
      - median:
          window_size: 5
          send_every: 1
          send_first_at: 1

      # Mocniejsze wygładzenie niż dotychczasowe alpha: 0.50,
      # ale bez sztywnego opóźnienia średniej kroczącej.
      - exponential_moving_average:
          alpha: 0.25
          send_every: 1
          send_first_at: 1
    lambda: |-
      const float v = id(wlot_air_velocity).state;
      if (isnan(v) || v <= 0.0f) return 0.0f;

      const float D = ${venturi_D_mm} / 1000.0f;
      const float A1 = 3.14159265f * D * D / 4.0f;
      return v * A1 * 3600.0f;

  - platform: template
    name: "Wylot - przepływ powietrza"
    id: wylot_airflow
    unit_of_measurement: "m³/h"
    accuracy_decimals: 1
    update_interval: 2s
    filters:
      # Usuwa krótkie piki i pojedyncze anomalie.
      - median:
          window_size: 5
          send_every: 1
          send_first_at: 1

      # Mocniejsze wygładzenie niż dotychczasowe alpha: 0.50,
      # ale bez sztywnego opóźnienia średniej kroczącej.
      - exponential_moving_average:
          alpha: 0.25
          send_every: 1
          send_first_at: 1
    lambda: |-
      const float v = id(wylot_air_velocity).state;
      if (isnan(v) || v <= 0.0f) return 0.0f;

      const float D = ${venturi_D_mm} / 1000.0f;
      const float A1 = 3.14159265f * D * D / 4.0f;
      return v * A1 * 3600.0f;

  # ---- Fan tachometers ----
  - platform: pulse_counter
    pin:
      number: GPIO34
      mode:
        input: true
        pullup: false
    name: "Wylot RPM"
    id: fan1_rpm
    unit_of_measurement: "rpm"
    accuracy_decimals: 0
    use_pcnt: true
    internal_filter: 10us
    update_interval: 5s
    count_mode:
      rising_edge: DISABLE
      falling_edge: INCREMENT
    filters:
      - multiply: 1.0

  - platform: pulse_counter
    pin:
      number: GPIO35
      mode:
        input: true
        pullup: false
    name: "Wlot RPM"
    id: fan2_rpm
    unit_of_measurement: "rpm"
    accuracy_decimals: 0
    use_pcnt: true
    internal_filter: 10us
    update_interval: 5s
    count_mode:
      rising_edge: DISABLE
      falling_edge: INCREMENT
    filters:
      - multiply: 1.0

output:
  # 2N3904 stage inverts the fan control signal, so outputs are inverted here.
  - platform: ledc
    id: fan1_pwm_output
    pin:
      number: GPIO33
      inverted: true
    frequency: 1000Hz
    min_power: 11%
    max_power: 100%
    zero_means_zero: true

  - platform: ledc
    id: fan2_pwm_output
    pin:
      number: GPIO32
      inverted: true
    frequency: 1000Hz
    min_power: 11%
    max_power: 100%
    zero_means_zero: true

fan:
  # Fan 1 = Wylot / slave in airflow-sync mode
  - platform: speed
    output: fan1_pwm_output
    id: fan1_pwm
    name: "Wylot PWM"
    speed_count: 100
    restore_mode: ALWAYS_OFF

  # Fan 2 = Wlot / master
  - platform: speed
    output: fan2_pwm_output
    id: fan2_pwm
    name: "Wlot PWM"
    speed_count: 100
    restore_mode: ALWAYS_OFF
    on_speed_set:
      - if:
          condition:
            lambda: |-
              return id(fan_sync_mode).current_option() == "Moc PWM" &&
                     id(fan1_pwm).speed != x;
          then:
            - fan.turn_on:
                id: fan1_pwm
                speed: !lambda 'return x;'

    on_turn_on:
      - if:
          condition:
            lambda: |-
              return id(fan_sync_mode).current_option() == "Moc PWM" &&
                     !id(fan1_pwm).state;
          then:
            - fan.turn_on:
                id: fan1_pwm
                speed: !lambda 'return id(fan2_pwm).speed;'

    on_turn_off:
      - if:
          condition:
            lambda: |-
              return id(fan_sync_mode).current_option() == "Moc PWM" &&
                     id(fan1_pwm).state;
          then:
            - fan.turn_off: fan1_pwm

switch:
  - platform: gpio
    id: recuperator_main_relay
    name: "Rekuperator zasilanie"
    pin:
      number: GPIO16
      inverted: false
    restore_mode: ALWAYS_OFF

  - platform: gpio
    id: belimo_open_relay
    name: "Belimo Open Relay Raw"
    internal: true
    pin:
      number: GPIO23
      inverted: false
    restore_mode: ALWAYS_OFF
    interlock: [belimo_close_relay]
    interlock_wait_time: 500ms

  - platform: gpio
    id: belimo_close_relay
    name: "Belimo Close Relay Raw"
    internal: true
    pin:
      number: GPIO22
      inverted: false
    restore_mode: ALWAYS_OFF
    interlock: [belimo_open_relay]
    interlock_wait_time: 500ms

  - platform: template
    name: "Zasilanie rekuperatora"
    id: recuperator_power
    icon: mdi:power
    optimistic: true
    restore_mode: ALWAYS_OFF
    turn_on_action:
      - switch.turn_on: recuperator_main_relay
      - delay: 1s
      - valve.close: bypass
    turn_off_action:
      - fan.turn_off: fan1_pwm
      - fan.turn_off: fan2_pwm
      - switch.turn_off: belimo_open_relay
      - switch.turn_off: belimo_close_relay
      - delay: 1s
      - switch.turn_off: recuperator_main_relay

select:
  - platform: template
    name: "Synchronizacja wentylatorów"
    id: fan_sync_mode
    icon: mdi:sync
    optimistic: true
    restore_value: false
    initial_option: "Brak"
    options:
      - "Brak"
      - "Moc PWM"
      - "Przepływ powietrza"
    set_action:
      # Przy zmianie trybu usuwamy pamięć poprzedniej regulacji.
      - globals.set:
          id: flow_sync_integral
          value: '0.0f'
      - globals.set:
          id: flow_sync_next_speed
          value: '-1'

      # W trybie PWM Wylot natychmiast przyjmuje stan i moc Wlotu.
      - if:
          condition:
            lambda: 'return x == "Moc PWM";'
          then:
            - if:
                condition:
                  fan.is_on: fan2_pwm
                then:
                  - fan.turn_on:
                      id: fan1_pwm
                      speed: !lambda 'return id(fan2_pwm).speed;'
                else:
                  - fan.turn_off: fan1_pwm

script:
  - id: belimo_open_script
    mode: restart
    then:
      - switch.turn_off: belimo_close_relay
      - switch.turn_on: belimo_open_relay
      - delay: ${belimo_pulse_time}
      - switch.turn_off: belimo_open_relay

  - id: belimo_close_script
    mode: restart
    then:
      - switch.turn_off: belimo_open_relay
      - switch.turn_on: belimo_close_relay
      - delay: ${belimo_pulse_time}
      - switch.turn_off: belimo_close_relay

valve:
  - platform: template
    name: "By-pass"
    id: bypass
    icon: mdi:valve
    optimistic: true
    has_position: false
    open_action:
      - script.stop: belimo_close_script
      - switch.turn_off: belimo_close_relay
      - script.execute: belimo_open_script
    close_action:
      - script.stop: belimo_open_script
      - switch.turn_off: belimo_open_relay
      - script.execute: belimo_close_script

interval:
  - interval: 5s
    startup_delay: 15s
    then:
      - if:
          condition:
            lambda: |-
              return id(fan_sync_mode).current_option() == "Przepływ powietrza";
          then:
            - lambda: |-
                // Wlot jest wartością zadaną.
                // Wylot jest wartością regulowaną.
                const float target = id(wlot_airflow).state;
                const float actual = id(wylot_airflow).state;

                id(flow_sync_next_speed) = -1;

                if (isnan(target) || isnan(actual)) return;

                // Fan 2 = Wlot. Jeśli Wlot nie pracuje, wyłącz Wylot.
                if (!id(fan2_pwm).state || target < 3.0f) {
                  id(flow_sync_integral) = 0.0f;
                  id(flow_sync_next_speed) = 0;
                  return;
                }

                // Dodatni błąd: Wylot ma za mały przepływ.
                float error = target - actual;

                // Zapobiega ciągłym małym korektom przy szumie pomiaru.
                if (fabsf(error) < 1.0f) error = 0.0f;

                const float kp = 0.12f;
                const float ki = 0.010f;

                float integral = id(flow_sync_integral) + error;
                if (integral > 150.0f) integral = 150.0f;
                if (integral < -150.0f) integral = -150.0f;
                id(flow_sync_integral) = integral;

                const float correction = kp * error + ki * integral;

                // Fan 1 = Wylot, jego PWM jest regulowane automatycznie.
                int next_speed = (int) roundf(id(fan1_pwm).speed + correction);

                if (next_speed < 11) next_speed = 11;
                if (next_speed > 100) next_speed = 100;

                id(flow_sync_next_speed) = next_speed;

            - if:
                condition:
                  lambda: 'return id(flow_sync_next_speed) == 0;'
                then:
                  - fan.turn_off: fan1_pwm

            - if:
                condition:
                  lambda: 'return id(flow_sync_next_speed) >= 11;'
                then:
                  - fan.turn_on:
                      id: fan1_pwm
                      speed: !lambda 'return id(flow_sync_next_speed);'

button:
  - platform: restart
    name: "Restart rekuperatora"

 

Subscribe
Notify of
guest

0 Comments

0
Would love your thoughts, please comment.x
()
x