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 na
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"