Starting Point# Our home uses a decentralized ventilation system from Ventomaxx with five reversing fans (ebmpapst VarioPro 4412 FGMPR ) that switch direction every ~70 seconds to recover heat via a regenerative core. The original room unit allowed only manual program/level changes at the wall.
Original Ventomaxx Wall Unit
This is how the fans look like:
ebmpapst VarioPro 4412 FGMPR
What needed to change# Manual mode/strength changes at the wall were impractical. The target was to expose per‑fan control and current mode visualization in Home Assistant, and to automate operation (e.g., reduce bedroom flow overnight).
Analysis# The wall controller drives fan speed and direction with a 2 kHz PWM where duty encodes both direction and magnitude:
0%: clockwise, full speed 50%: stop 100%: counter‑clockwise, full speed Schematic# The custom controller (ESP8266 + PWM outputs) replaces the wall unit’s fan drive, one PWM channel per room. ESPHome exposes a native API to Home Assistant; HA inputs drive setpoints per room; a script enforces the pendulum timing
Schema/Flow
Hardware# Prototype# Verified PWM behavior and isolation on perfboard to validate signal levels and noise immunity before committing to PCB.
Prototype on Perfboard
Custom PCB# A KiCad board consolidates power, five PWM channels at ~1.92 kHz, headers to the fan harness, and the ESP module for reliability and serviceability.
Schematics# KiCAD Schematics
PCB Layout# KiCAD PCB Layout
ESPhome program# Core Principal# One PWM output per fan at ~1.92 kHz (close to measured 2 kHz). Home Assistant input_numbers define “level” per room; a script maps those to PWM setpoints around a 50% neutral. A continuous loop enforces the cycle: 10 s pause at neutral, 70 s direction A, 10 s pause, 70 s direction B—aligning with typical 70–90 s reversal for regenerative HRV Code# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
substitutions :
ip : redacted
node_name : ventomaxx
friendly_name : Ventomaxx
api_encryption_key : redacted
ota_password : redacted
packages :
device_base : ! include / config / common / device_base . yaml
esphome :
name : $ { node_name }
on_boot :
priority : - 100
then :
- script . execute : my_loop
esp8266 :
board : nodemcuv2
framework :
version : recommended
sensor :
- platform : homeassistant
name : "Bedroom Level"
entity_id : input_number . ventilation_bedroom
id : bedroom_level
on_value :
then :
- script . execute : my_loop
- platform : homeassistant
name : "Kitchen Level"
entity_id : input_number . ventilation_kitchen
id : kitchen_level
on_value :
then :
- script . execute : my_loop
- platform : homeassistant
name : "Livingroom Level"
entity_id : input_number . ventilation_livingroom
id : livingroom_level
on_value :
then :
- script . execute : my_loop
- platform : homeassistant
name : "Nursery Level"
entity_id : input_number . ventilation_nursery
id : nursery_level
on_value :
then :
- script . execute : my_loop
- platform : homeassistant
name : "Office Level"
entity_id : input_number . ventilation_office
id : office_level
on_value :
then :
- script . execute : my_loop
output :
- platform : esp8266_pwm
pin : GPIO5 # D1
frequency : 1920 Hz
id : pwm_output_bedroom
- platform : esp8266_pwm
pin : GPIO4 # D2
frequency : 1920 Hz
id : pwm_output_kitchen
- platform : esp8266_pwm
pin : GPIO14 # D5
frequency : 1920 Hz
id : pwm_output_livingroom
- platform : esp8266_pwm
pin : GPIO13 # D7
frequency : 1920 Hz
id : pwm_output_nursery
- platform : esp8266_pwm
pin : GPIO12 # D6
frequency : 1920 Hz
id : pwm_output_office
fan :
- platform : speed
output : pwm_output_bedroom
name : "Fan Bedroom"
speed_count : 100
id : fan_bedroom
internal : true
restore_mode : RESTORE_DEFAULT_OFF
- platform : speed
output : pwm_output_kitchen
name : "Fan Kitchen"
speed_count : 100
id : fan_kitchen
internal : true
restore_mode : RESTORE_DEFAULT_OFF
- platform : speed
output : pwm_output_livingroom
name : "Fan Livingroom"
speed_count : 100
id : fan_livingroom
internal : true
restore_mode : RESTORE_DEFAULT_ON
- platform : speed
output : pwm_output_nursery
name : "Fan Nursery"
speed_count : 100
id : fan_nursery
internal : true
restore_mode : RESTORE_DEFAULT_OFF
- platform : speed
output : pwm_output_office
name : "Fan Office"
speed_count : 100
id : fan_office
internal : true
restore_mode : RESTORE_DEFAULT_ON
script :
- id : my_loop
mode : restart
then :
- while :
condition :
lambda : |-
return true ;
then :
### Start ###
- logger . log : " "
- logger . log : "Interval Begin"
### Step 1: Pause ###
- logger . log : "Speed Off for 10 Sec"
- fan . turn_on :
id : fan_bedroom
speed : 50
- fan . turn_on :
id : fan_kitchen
speed : 50
- fan . turn_on :
id : fan_livingroom
speed : 50
- fan . turn_on :
id : fan_nursery
speed : 50
- fan . turn_on :
id : fan_office
speed : 50
- delay : 10 sec
### Step 2: First Direction ###
- logger . log : "Speed inbound for 70 Sec"
- lambda : |-
auto call_bedroom = id ( fan_bedroom ). turn_on ();
call_bedroom . set_speed ( 50 - id ( bedroom_level ). state );
call_bedroom . perform ();
auto call_kitchen = id ( fan_kitchen ). turn_on ();
call_kitchen . set_speed ( 50 - id ( kitchen_level ). state );
call_kitchen . perform ();
auto call_livingroom = id ( fan_livingroom ). turn_on ();
call_livingroom . set_speed ( 50 + id ( livingroom_level ). state );
call_livingroom . perform ();
auto call_nursery = id ( fan_nursery ). turn_on ();
call_nursery . set_speed ( 50 - id ( nursery_level ). state );
call_nursery . perform ();
auto call_office = id ( fan_office ). turn_on ();
call_office . set_speed ( 50 + id ( office_level ). state );
call_office . perform ();
- delay : 70 sec
### Step 3: Pause ###
- logger . log : "Speed Off for 10 Sec"
- fan . turn_on :
id : fan_bedroom
speed : 50
- fan . turn_on :
id : fan_kitchen
speed : 50
- fan . turn_on :
id : fan_livingroom
speed : 50
- fan . turn_on :
id : fan_nursery
speed : 50
- fan . turn_on :
id : fan_office
speed : 50
- delay : 10 sec
### Step 4: Second Direction ###
- logger . log : "Speed outbound for 70 Sec"
- lambda : |-
auto call_bedroom = id ( fan_bedroom ). turn_on ();
call_bedroom . set_speed ( 50 + id ( bedroom_level ). state );
call_bedroom . perform ();
auto call_kitchen = id ( fan_kitchen ). turn_on ();
call_kitchen . set_speed ( 50 + id ( kitchen_level ). state );
call_kitchen . perform ();
auto call_livingroom = id ( fan_livingroom ). turn_on ();
call_livingroom . set_speed ( 50 - id ( livingroom_level ). state );
call_livingroom . perform ();
auto call_nursery = id ( fan_nursery ). turn_on ();
call_nursery . set_speed ( 50 + id ( nursery_level ). state );
call_nursery . perform ();
auto call_office = id ( fan_office ). turn_on ();
call_office . set_speed ( 50 - id ( office_level ). state );
call_office . perform ();
- delay : 70 sec
### End ###
- logger . log : "Interval End"
- logger . log : " "
Home-Assistant# Frontend# ESPHome’s native API auto‑discovers entities. Then it’s just a matter of configuring the frontend:
Fan Controls in Home-Assistant
Automatization# I’am using the status of my shutters to determine if I want to reduze the strength my fans in the bedroom. This lookis like this in Home-Assistant:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
alias : Ventilation - Ventomaxx
description : ""
triggers :
- device_id : dd3b07e7b906e21b716db5b866fe6871
domain : cover
entity_id : e9766c2e1b7a0e636c296aa6689556fb
type : closed
id : cover_bedroom_closed
trigger : device
- device_id : dd3b07e7b906e21b716db5b866fe6871
domain : cover
entity_id : e9766c2e1b7a0e636c296aa6689556fb
type : opened
id : cover_bedroom_opened
trigger : device
- device_id : ebf922f99be1e5004e3c6387f8aa374b
domain : cover
entity_id : c93c90393ee01dec1a2894977e3b593d
type : closed
trigger : device
id : cover_nursery_closed
- device_id : ebf922f99be1e5004e3c6387f8aa374b
domain : cover
entity_id : c93c90393ee01dec1a2894977e3b593d
type : opened
trigger : device
id : cover_nursery_opened
- type : turned_off
device_id : b48b11cc3f8a2c6f8f575a21b0e6e4bd
entity_id : e4fec3c726da7892b2e587919654ebc0
domain : switch
id : computer_tom_off
trigger : device
- type : turned_on
device_id : b48b11cc3f8a2c6f8f575a21b0e6e4bd
entity_id : e4fec3c726da7892b2e587919654ebc0
domain : switch
id : computer_tom_on
trigger : device
- entity_id :
- zone.home
to : "0"
id : zone_nobody_home
enabled : false
trigger : state
- entity_id :
- zone.home
to : null
id : zone_someone_home
from : "0"
enabled : false
trigger : state
conditions : []
actions :
- choose :
- conditions :
- condition : trigger
id :
- cover_bedroom_closed
sequence :
- data :
value : 15
target :
entity_id : input_number.ventilation_bedroom
action : input_number.set_value
- conditions :
- condition : trigger
id :
- cover_bedroom_opened
sequence :
- data :
value : 50
target :
entity_id : input_number.ventilation_bedroom
action : input_number.set_value
- conditions :
- condition : trigger
id :
- cover_nursery_closed
sequence :
- data :
value : 15
target :
entity_id : input_number.ventilation_nursery
action : input_number.set_value
- conditions :
- condition : trigger
id :
- cover_nursery_opened
sequence :
- data :
value : 50
target :
entity_id : input_number.ventilation_nursery
action : input_number.set_value
- conditions :
- condition : trigger
id :
- computer_tom_off
sequence :
- data :
value : 35
target :
entity_id : input_number.ventilation_office
action : input_number.set_value
- conditions :
- condition : trigger
id :
- computer_tom_on
sequence :
- data :
value : 15
action : input_number.set_value
target :
entity_id : input_number.ventilation_office
- conditions :
- condition : trigger
id :
- zone_nobody_home
sequence :
- target :
entity_id : scene.ventilation_max
metadata : {}
action : scene.turn_on
- conditions :
- condition : trigger
id :
- zone_someone_home
sequence :
- target :
entity_id : scene.ventilation_typical
metadata : {}
action : scene.turn_on
mode : single
Potential future automatizations# Demand mode : raise kitchen/living when humidity spikes; reduce office when workstation off.Kitchen mode : raise kitchen when stove is on/draws power.Away mode : scene sets all levels low while preserving air change to protect the building fabric.