All
intermediateMicrocontrollers9-Sept-2026

Turning a Tion 3S Breezer into a CO2-Controlled Smart Supply Ventilator with ESPHome

Author
Anton Shagaev
Tashkent, UZ
3 hours
35
35
Cover
Hello friends! A breezer like the Tion 3S is a wall-mounted forced-air supply ventilator. Its main job is to pull fresh outdoor air into the room, filter out dust, allergens, and odors using a thick filter stack, and warm it up to a comfortable temperature with a built-in ceramic heater. It solves the classic problem of tightly sealed modern apartments: you either keep the windows closed and deal with stuffy air, morning headaches, and high CO2 levels, or you open the windows and let in freezing drafts, street noise, and pollution. It is a solid piece of hardware for indoor air quality, but out of the box, its smart capabilities are severely lacking.
You can technically get automatic CO2 control by purchasing Tion's official MagicAir base station, but that costs roughly $270 to $360 depending on the model. Compared to a custom Zigbee CO2 sensor like the DIYRuZ_AirSense (built on a Texas Instruments chip and exposing a clean MQTT entity) which sets you back around $69 if bought ready-made, or just $25-$35 for the components if you build it yourself - the official route is unreasonably expensive. More importantly, since the primary control system in my house is already Home Assistant, I explicitly wanted the breezer integrated natively into HA, rather than running a parallel, closed-off ecosystem just for ventilation.
The Tion 3S was never designed to talk to Home Assistant at all. The only reason it works with HA today is that a small group of enthusiasts reverse-engineered the breezer's Bluetooth LE protocol and built a custom integration on top of it, 'TionAPI/HA-tion'. It is a solid work and it does let you flip fan speeds and heater modes from HA, but it inherits every limitation of BLE: it's slow to respond, the connection drops constantly, and - since our HA server sits in a different room from the breezer - the server's Bluetooth radio sometimes simply can't reach it through the walls at all. Every automation built on top of that link inherited the same flakiness.
Swapping the BLE link for a NodeMCU ESP8266 talking straight to the breezer's UART header fixes all three problems at once: it's on Wi-Fi instead of relying on BLE range, commands land in well under a second instead of several seconds, and the connection to Home Assistant simply doesn't drop. That reliability is what actually makes the rest of this project possible - once the breezer responds instantly and consistently, you can wire it up to a real CO2 sensor and Home Assistant automations and turn a dumb, on/off supply ventilator into something that actually reacts to air quality in the room by pulling in and pre-heating fresh outdoor air on its own.

Steps

1

Opening up the breezer

Unscrewing the bottom cover to access the control board
Once the power is completely disconnected, pop the front cover off. You don't actually need to pull out the filter stack to reach the electronics. Instead, unscrew the screws from the bottom cover as shown below. The control board is located right under this cover on the right side.
Unplug the breezer from the 220V wall outlet before doing anything else.
2

First view of the control board

First view of the control board with the stock BLE module connected
Once you remove the bottom cover, you get your first view of the electronics bay. Down in the corner sits the main control board - the one with the buzzer, the coin-cell backup battery, and the ribbon cable. Here you can see the stock BLE module still clipped onto the UART header.
3

Removing the stock BLE module

The stock BLE module removed from the breezer
BLE module
This BLE module is exactly the part we are bypassing. Carefully pull it off the pins to remove it from the breezer. Here is how the main control board looks with the stock module removed, clearly exposing the UART pins.
4

Wiring the NodeMCU to the Tion 3S

NodeMCU in the 3D-printed case connected to the UART pins
ESP8266 top side
ESP8266 bottom side
I printed a small enclosure for the NodeMCU (the iBoxit case from MakerWorld) to keep the board clean and protected. Wire the 4-wire UART header inside the unit (TX, RX, GND, 3.3V) straight to the NodeMCU's hardware UART pins (TX/RX), GND, and 3.3V for power. No level shifting needed - both sides run at 3.3V logic, and the breezer can power the NodeMCU directly. Here is the NodeMCU in its printed box already connected to the breezer's UART pins.
5

Flashing the Firmware (ESPHome)

Before Home Assistant can see the device, we need to compile and flash the NodeMCU via a USB cable. For this step, you will need a Home Assistant instance with the ESPHome integration enabled, the ESPHome CLI (I ran it via Docker using 'ghcr.io/esphome/esphome:2024.12.0'), and the 'dentra/esphome-tion' external component. All the firmware compilation and flashing steps were done on a computer running Linux machine directly through the terminal.
Note: You only need to plug the NodeMCU into your computer via USB for this very first flash. Once the firmware is on the board and it connects to your Wi-Fi, you can install it in the breezer. All future firmware updates and config tweaks can be done over-the-air (OTA) via Wi-Fi by simply pointing the ESPHome CLI to the NodeMCU's IP address.
1. Preparing the working directory
First, create a dedicated folder for the project and navigate into it:
tinkster@almalinux:~#
1
mkdir -p ~/tion-esphome && cd ~/tion-esphome
2. Creating the secrets.yaml file
Inside the tion-esphome directory, create a file named secrets.yaml to hold your Wi-Fi credentials. ESPHome will pull from this file during the build.
secrets.yaml
1
wifi_ssid: "YOUR_WIFI_NAME"
2
wifi_password: "YOUR_WIFI_PASSWORD"
3. Creating the main configuration file
In the same directory, create the tion-uart.yaml file. I based the whole firmware on the packages/tion_3s.yaml bundle from dentra/esphome-tion, which already ships the UART protocol implementation. I also baked the CO2 automation logic directly into the ESPHome node using an interval block that reads a Home Assistant sensor.
Here is the final, fully working tion-uart.yaml end to end:
tion-uart.yaml
1
substitutions:
2
node_name: tion-3s
3
friendly_name: Tion 3S
4
name: tion-3s
5
tion_conn: uart
6
board_code: nodemcuv2
7
board: nodemcuv2
8
tion_type: 3s
9
tx_pin: TX
10
rx_pin: RX
11
wifi_ssid: !secret wifi_ssid
12
wifi_password: !secret wifi_password
13
wifi_ap_password: tion-32489-3s
14
15
packages:
16
tion:
17
url: https://github.com/dentra/esphome-tion
18
ref: 2024.12.0
19
files:
20
- packages/wifi_signal.yaml
21
- packages/uptime.yaml
22
- packages/restart.yaml
23
- packages/tion_energy.yaml
24
- packages/tion_enable_antifreeze.yaml
25
- packages/base.yaml
26
- packages/esp8266_uart.yaml
27
- packages/tion_3s.yaml
28
29
api:
30
31
wifi:
32
ssid: $wifi_ssid
33
password: $wifi_password
34
ap:
35
password: $wifi_ap_password
36
37
vport:
38
- platform: tion_3s_uart
39
id: tion_uart_vport
40
uart_id: tion_uart
41
42
tion:
43
type: 3s
44
id: tion_api
45
vport_id: tion_uart_vport
46
update_interval: 30s
47
batch_timeout: 200ms
48
state_timeout: 3s
49
force_update: false
50
presets:
51
home: { fan_speed: 2, temperature: 20, heater: False }
52
sleep: { fan_speed: 1, temperature: 18, heater: False }
53
away: { fan_speed: 1, temperature: 10, heater: False }
54
55
climate:
56
- platform: tion
57
id: tion_climate
58
name: None
59
enable_heat_cool: false
60
enable_fan_auto: true
61
62
fan:
63
- platform: tion
64
id: tion_fan
65
name: Fan
66
on_state:
67
- lambda: |-
68
if (id(correcting_fan_speed)) {
69
id(correcting_fan_speed) = false;
70
id(fan_was_on) = id(tion_fan)->state;
71
if (id(tion_fan)->state) {
72
id(last_fan_speed) = id(tion_fan)->speed;
73
}
74
return;
75
}
76
bool is_on = id(tion_fan)->state;
77
int spd = id(tion_fan)->speed;
78
bool just_turned_on = is_on && !id(fan_was_on);
79
if (just_turned_on && !id(co2_auto_writing) && spd == 6 && id(last_fan_speed) != 6) {
80
id(correcting_fan_speed) = true;
81
auto call = id(tion_fan)->turn_on();
82
call.set_speed(id(last_fan_speed));
83
call.perform();
84
} else if (is_on && !id(co2_auto_writing)) {
85
id(last_fan_speed) = spd;
86
}
87
id(fan_was_on) = is_on;
88
89
sensor:
90
- platform: homeassistant
91
id: playroom_co2
92
entity_id: sensor.0x00158d0008f3a21b_co2
93
internal: true
94
- platform: template
95
id: tion_current_temperature_sensor
96
name: Current Temperature
97
unit_of_measurement: "°C"
98
device_class: temperature
99
state_class: measurement
100
update_interval: 30s
101
lambda: |-
102
return id(tion_climate).current_temperature;
103
- platform: template
104
id: tion_target_temperature_sensor
105
name: Target Temperature
106
unit_of_measurement: "°C"
107
device_class: temperature
108
state_class: measurement
109
update_interval: 30s
110
lambda: |-
111
return id(tion_climate).target_temperature;
112
- platform: tion
113
id: tion_outdoor_temperature
114
type: outdoor_temperature
115
name: Outdoor Temperature
116
- platform: tion
117
id: tion_heater_power
118
type: heater_power
119
name: Heater Power
120
- platform: tion
121
id: tion_productivity
122
type: productivity
123
name: Productivity
124
- platform: tion
125
id: tion_filter_time_left_days
126
type: filter_time_left_days
127
name: Filter Time Left
128
- platform: tion
129
id: tion_boost_time_left
130
type: boost_time_left
131
name: Boost Time Left
132
- platform: tion
133
id: tion_fan_speed
134
type: fan_speed
135
name: Fan Speed
136
137
switch:
138
- platform: tion
139
id: tion_buzzer
140
type: sound
141
name: Buzzer
142
- platform: tion
143
id: tion_boost
144
type: boost
145
name: Boost
146
- platform: template
147
id: auto_co2_switch
148
name: "Auto Mode (CO2)"
149
icon: mdi:fan-auto
150
optimistic: true
151
lambda: |-
152
return id(auto_co2_enabled);
153
turn_on_action:
154
- lambda: 'id(auto_co2_enabled) = true;'
155
turn_off_action:
156
- lambda: 'id(auto_co2_enabled) = false;'
157
158
number:
159
- platform: tion
160
id: tion_boost_time
161
type: boost_time
162
name: Boost Time
163
initial_value: 20min
164
restore_value: true
165
- platform: tion
166
id: tion_auto_min_fan_speed
167
type: auto_min_fan_speed
168
name: Auto Min Fan Speed
169
initial_value: 1
170
restore_value: true
171
- platform: tion
172
id: tion_auto_max_fan_speed
173
type: auto_max_fan_speed
174
name: Auto Max Fan Speed
175
initial_value: 6
176
restore_value: true
177
178
select:
179
- platform: tion
180
id: tion_air_intake
181
type: air_intake
182
name: Air Intake
183
184
binary_sensor:
185
- platform: tion
186
id: tion_state_problem
187
type: state
188
name: State Problem
189
- platform: tion
190
id: tion_filter_worn_out
191
type: filter
192
name: Filter Worn Out
193
- platform: tion
194
id: tion_heater_state
195
type: heater
196
name: Heater State
197
- platform: tion
198
id: tion_power_state
199
type: power
200
name: Power State
201
202
text_sensor:
203
- platform: tion
204
id: tion_firmware_version
205
type: firmware_version
206
name: Version
207
- platform: tion
208
id: tion_errors
209
type: errors
210
name: Errors
211
212
button:
213
- platform: tion
214
id: tion_reset_filter
215
type: reset_filter
216
name: Reset Filter
217
confirm:
218
name: Reset Filter Confirm
219
220
globals:
221
- id: auto_co2_enabled
222
type: bool
223
restore_value: true
224
initial_value: 'true'
225
- id: last_fan_speed
226
type: int
227
restore_value: true
228
initial_value: '2'
229
- id: fan_was_on
230
type: bool
231
restore_value: false
232
initial_value: 'false'
233
- id: co2_auto_writing
234
type: bool
235
restore_value: false
236
initial_value: 'false'
237
- id: correcting_fan_speed
238
type: bool
239
restore_value: false
240
initial_value: 'false'
241
242
interval:
243
- interval: 60s
244
then:
245
- if:
246
condition:
247
- binary_sensor.is_on: tion_power_state
248
- lambda: 'return id(tion_air_intake).state != "outdoor";'
249
then:
250
- select.set:
251
id: tion_air_intake
252
option: "outdoor"
253
- interval: 30s
254
then:
255
- lambda: |-
256
if (!id(auto_co2_enabled)) return;
257
if (!id(tion_power_state).state) return;
258
float co2 = id(playroom_co2).state;
259
int speed;
260
if (co2 > 1600) speed = 6;
261
else if (co2 > 1400) speed = 5;
262
else if (co2 > 1200) speed = 4;
263
else if (co2 > 1000) speed = 3;
264
else if (co2 > 800) speed = 2;
265
else speed = 1;
266
id(co2_auto_writing) = true;
267
auto call = id(tion_fan).turn_on();
268
call.set_speed(speed);
269
call.perform();
270
id(co2_auto_writing) = false;
4. Compiling and flashing
With the NodeMCU connected via USB and the tion-esphome folder containing both the secrets.yaml and tion-uart.yaml files, you can run the ESPHome Docker container from within that same folder to compile and flash the firmware:
tinkster@almalinux:~#
1
docker run --rm -it --device=/dev/ttyUSB0 -v "$PWD":/config ghcr.io/esphome/esphome:2024.12.0 run tion-uart.yaml
5. Subsequent updates over Wi-Fi (OTA)
Once the initial firmware is flashed via USB and the NodeMCU is installed inside the breezer and connected to your Wi-Fi network, you no longer need physical USB access. To update or tweak the configuration later, simply pass the device's IP address using the --device flag in your terminal:
tinkster@almalinux:~#
1
docker run --rm -it --network host -v "$PWD":/config ghcr.io/esphome/esphome:2024.12.0 run tion-uart.yaml --device 192.168.0.121
6

Home Assistant Setup

Once the board is flashed and connected to Wi-Fi, Home Assistant will automatically discover it via ESPHome.
To cleanly expose the controls, I built a vertical stack card for the dashboard. It dynamically hides the manual fan dropdown when our new "Auto Mode" switch is flipped on (preventing manual overrides while CO2 logic is running) and dims out the air intake selector using card-mod. It strictly focuses on the breezer itself and the CO2 sensor guiding it.
tinkster@almalinux:~#
1
type: vertical-stack
2
cards:
3
- type: conditional
4
conditions:
5
- entity: switch.tion_3s_auto_mode_co2
6
state: "off"
7
card:
8
type: thermostat
9
entity: climate.tion_3s
10
features:
11
- type: climate-fan-modes
12
style: dropdown
13
- type: climate-hvac-modes
14
name: Supply ventilation
15
- type: conditional
16
conditions:
17
- entity: switch.tion_3s_auto_mode_co2
18
state: "on"
19
card:
20
type: thermostat
21
entity: climate.tion_3s
22
features:
23
- type: climate-hvac-modes
24
name: Supply ventilation
25
- type: entities
26
entities:
27
- entity: switch.tion_3s_auto_mode_co2
28
secondary_info: last-changed
29
name: Auto mode
30
- entity: select.tion_3s_air_intake
31
name: Air intake
32
card_mod:
33
style: |
34
:host {
35
pointer-events: {{ 'none' if is_state('switch.tion_3s_auto_mode_co2', 'on') else 'auto' }};
36
opacity: {{ '0.4' if is_state('switch.tion_3s_auto_mode_co2', 'on') else '1' }};
37
}
38
- entity: sensor.0x00158d0008f3a21b_co2
39
name: CO2 level
40
secondary_info: last-changed
41
- entity: sensor.tion_3s_fan_speed
42
name: Current speed
43
secondary_info: last-changed
44
- entity: sensor.tion_3s_outdoor_temperature
45
secondary_info: last-changed
46
name: Intake temperature
47
- entity: sensor.tion_3s_current_temperature
48
secondary_info: last-changed
49
name: Outlet temperature
50
state_color: true
I also migrated my old window logic. Instead of running percentage-based CO2 automations in HA (which are now handled natively on the ESP), Home Assistant just makes sure the breezer turns off when a window opens, and kicks back on when both are confirmed closed:
tinkster@almalinux:~#
1
- alias: Turn off ventilation when any loggia window opens
2
triggers:
3
- trigger: device
4
type: opened
5
device_id: 112233445566778899aabbccddeeff00
6
entity_id: aabbccddeeff00112233445566778899
7
domain: binary_sensor
8
- trigger: device
9
type: opened
10
device_id: 99887766554433221100ffeeddccbbaa
11
entity_id: ffeeddccbbaa99887766554433221100
12
domain: binary_sensor
13
conditions: []
14
actions:
15
- action: fan.turn_off
16
target:
17
entity_id: fan.tion_3s_fan
18
19
- alias: Turn on ventilation once both loggia windows are closed
20
triggers:
21
- trigger: device
22
type: not_opened
23
device_id: 112233445566778899aabbccddeeff00
24
entity_id: aabbccddeeff00112233445566778899
25
domain: binary_sensor
26
- trigger: device
27
type: not_opened
28
device_id: 99887766554433221100ffeeddccbbaa
29
entity_id: ffeeddccbbaa99887766554433221100
30
domain: binary_sensor
31
conditions:
32
- condition: device
33
type: is_not_open
34
device_id: 112233445566778899aabbccddeeff00
35
entity_id: aabbccddeeff00112233445566778899
36
domain: binary_sensor
37
- condition: device
38
type: is_not_open
39
device_id: 99887766554433221100ffeeddccbbaa
40
entity_id: ffeeddccbbaa99887766554433221100
41
domain: binary_sensor
42
- condition: state
43
entity_id: fan.tion_3s_fan
44
state: 'off'
45
actions:
46
- action: fan.turn_on
47
target:
48
entity_id: fan.tion_3s_fan
49
- delay:
50
seconds: 15
51
- action: select.select_option
52
target:
53
entity_id: select.tion_3s_air_intake
54
data:
55
option: "outdoor"

Conclusion

Eliminating the BLE connection was the core goal of this project. Since the Tion 3S wasn't designed as a smart-home appliance, poor Bluetooth range and reliability made early community integrations almost unusable. Upgrading to a NodeMCU over UART and Wi-Fi transforms the breezer into a smart device.
The dentra/esphome-tion component gets you 95% of the way there for a Tion 3S with zero fuss. However, getting to the perfectly stable firmware required patching several issues along the way.
Bug #1: Build fails because of a broken ETL library reference
The very first compile choked trying to resolve a PlatformIO library dependency called etlcpp/Embedded Template Library at version 20.35.8. Since the external component's own generated etl module is what issues that call, I patched it directly to point at the GitHub source and pinned it to a specific commit hash:
tinkster@almalinux:~#
1
sudo chown -R "$USER":"$USER" .esphome
2
3
ETL_PY="$(find .esphome -path '*/components/etl/__init__.py' -print -quit)"
4
5
cp "$ETL_PY" "${ETL_PY}.bak"
6
7
sed -i '/cg.add_library("etlcpp\/Embedded Template Library", "20.35.8")/c\
8
cg.add_library(\
9
name="Embedded Template Library",\
10
repository="https://github.com/ETLCPP/etl.git#20.35.8",\
11
version=None,\
12
)' "$ETL_PY"
13
14
sudo sed -i 's|cg.add_library("etlcpp/Embedded Template Library", "20.35.8")|cg.add_library("https://github.com/ETLCPP/etl.git#e74f8b46", None)|' \
15
.esphome/external_components/d18ce5a7/components/etl/__init__.py
Bug #2: The UART flood ("prev frame was not recv, may lead to crash")
This was the absolute main and most critical bug of the entire project. Because a rogue second polling loop inside the tion_3s_uart vport was firing request_command4() independently on its own timer, overlapping frames flooded the bus. This completely broke normal device operation and risked crashing or locking up the breezer's MCU.
The console filled up with warnings on almost every single request cycle. Grepping for the actual request call across every external component pointed me at the real source: the tion_3s_uart vport has its own update() override that fires request_command4() on its own internal timer, completely independent of the main tion component's own polling loop.
tinkster@almalinux:~#
1
sudo chown -R "$USER":"$USER" .esphome
2
3
ETL_PY="$(find .esphome -path '*/components/etl/__init__.py' -print -quit)"
4
5
cp "$ETL_PY" "${ETL_PY}.bak"
6
7
sed -i '/cg.add_library("etlcpp\/Embedded Template Library", "20.35.8")/c\
8
cg.add_library(\
9
name="Embedded Template Library",\
10
repository="https://github.com/ETLCPP/etl.git#20.35.8",\
11
version=None,\
12
)' "$ETL_PY"
13
14
sudo sed -i 's|cg.add_library("etlcpp/Embedded Template Library", "20.35.8")|cg.add_library("https://github.com/ETLCPP/etl.git#e74f8b46", None)|' \
15
.esphome/external_components/d18ce5a7/components/etl/__init__.py
16
17
sed -i '/super_t::write(\*frame, datav.size());/i\ this->await_frame_ = true;' \
18
.esphome/external_components/9d181271/components/tion/tion_vport_uart.h
19
20
cp .esphome/external_components/9d181271/components/tion_3s_uart/tion_3s_uart_vport.h \
21
.esphome/external_components/9d181271/components/tion_3s_uart/tion_3s_uart_vport.h.orig
22
23
sed -i 's|void update() override { this->api_->request_command4(); }|void update() override {}|' \
24
.esphome/external_components/9d181271/components/tion_3s_uart/tion_3s_uart_vport.h
The fix was to stop that second, redundant polling loop entirely and let the main tion component be the only thing driving communication.
Bug #3: The native "Auto Mode" switch won't stay on
I flipped the native Auto Mode switch on, and it silently flipped itself back off.
tinkster@almalinux:~#
1
[D][switch:012]: 'Auto Mode' Turning ON.
2
[D][tion-api:181]: New auto state OFF -> ON
3
[D][switch:012]: 'Auto Mode' Turning ON.
4
[D][tion-api:181]: New auto state OFF -> ON
Every single 5-second check found the flag already back to OFF. Without a physical MagicAir module talking to the breezer, the firmware treats the auto mode as unsupported and keeps resetting it. The fix (included in the final configuration above) was to bypass the hardware flag entirely, creating a purely software switch backed by a global variable, and an interval that reads a CO2 sensor piped in from Home Assistant to set the fan speed directly.
Bug #3: The intake damper drifts and the fan "always jumps to max speed"
The select for intake mode occasionally drifted away from outdoor on its own. A simple 60-second interval was added to re-assert it to outdoor. Additionally, toggling the fan on/off from a plain Home Assistant tumble switch made ESPHome call turn_on() with no explicit speed, defaulting to 6/6. This was patched with a small state machine in the on_state lambda to remember the last manually set speed and correct it back if a bare turn_on() lands on max speed incorrectly.
Update 10-Sep-2026: Multi-Sensor Air Quality Control & Accelerated Reporting
I have updated the firmware logic to factor in more than just CO2. Since the main objective is overall indoor air quality, I added an IKEA VINDSTYRKA to the mix via Zigbee2MQTT. It exposes reliable VOC index and PM2.5 measurements alongside the standard metrics.
To ensure the breezer reacts instantly to air quality drops, I also accelerated the reporting intervals for both the DIYRuZ_AirSense (CO2) and the IKEA VINDSTYRKA (VOC, PM2.5) sensors. By default, these sensors can be quite lazy, sometimes waiting up to an hour to report data if the changes are gradual. Through the Zigbee2MQTT "Reporting" tab for each cluster, I enforced much more aggressive polling parameters:
Min rep interval: 5 (seconds)
Max rep interval: 60 (seconds)
Min rep change: 10 (for CO2 to prevent micro-fluctuation noise) and 1 (for PM2.5 and VOC).
This setup forces the sensors to report data to Home Assistant at least once a minute, or within 5 seconds if a noticeable change occurs. This makes the ESPHome automations incredibly responsive, allowing the breezer to spool up the fan the moment the air quality begins to degrade.
The ESPHome node now pulls all three parameters from Home Assistant (CO2, VOC, and PM2.5). The custom polling loop evaluates the required fan speed for each individual pollutant and dynamically picks the highest required speed among them (std::max({speed_co2, speed_voc, speed_pm25})). To keep the dashboard clean, I also added a text sensor that outputs the specific "reason" (which pollutant is currently triggering the highest fan speed).
Here is the updated, fully working tion-uart.yaml configuration:
tion-uart.yaml
1
substitutions:
2
node_name: tion-3s
3
friendly_name: Tion 3S
4
name: tion-3s
5
tion_conn: uart
6
board_code: nodemcuv2
7
board: nodemcuv2
8
tion_type: 3s
9
tx_pin: TX
10
rx_pin: RX
11
wifi_ssid: !secret wifi_ssid
12
wifi_password: !secret wifi_password
13
wifi_ap_password: tion-0123-3s
14
15
packages:
16
tion:
17
url: https://github.com/dentra/esphome-tion
18
ref: 2024.12.0
19
files:
20
- packages/wifi_signal.yaml
21
- packages/uptime.yaml
22
- packages/restart.yaml
23
- packages/tion_energy.yaml
24
- packages/tion_enable_antifreeze.yaml
25
- packages/base.yaml
26
- packages/esp8266_uart.yaml
27
- packages/tion_3s.yaml
28
29
api:
30
31
wifi:
32
ssid: $wifi_ssid
33
password: $wifi_password
34
ap:
35
password: $wifi_ap_password
36
37
vport:
38
- platform: tion_3s_uart
39
id: tion_uart_vport
40
uart_id: tion_uart
41
42
tion:
43
type: 3s
44
id: tion_api
45
vport_id: tion_uart_vport
46
update_interval: 30s
47
batch_timeout: 200ms
48
state_timeout: 3s
49
force_update: false
50
presets:
51
home: { fan_speed: 2, temperature: 20, heater: False }
52
sleep: { fan_speed: 1, temperature: 18, heater: False }
53
away: { fan_speed: 1, temperature: 10, heater: False }
54
55
climate:
56
- platform: tion
57
id: tion_climate
58
name: None
59
enable_heat_cool: false
60
enable_fan_auto: true
61
62
fan:
63
- platform: tion
64
id: tion_fan
65
name: Fan
66
on_state:
67
- lambda: |-
68
if (id(correcting_fan_speed)) {
69
id(correcting_fan_speed) = false;
70
id(fan_was_on) = id(tion_fan)->state;
71
if (id(tion_fan)->state) {
72
id(last_fan_speed) = id(tion_fan)->speed;
73
}
74
return;
75
}
76
bool is_on = id(tion_fan)->state;
77
int spd = id(tion_fan)->speed;
78
bool just_turned_on = is_on && !id(fan_was_on);
79
if (just_turned_on && !id(auto_writing) && spd == 6 && id(last_fan_speed) != 6) {
80
id(correcting_fan_speed) = true;
81
auto call = id(tion_fan)->turn_on();
82
call.set_speed(id(last_fan_speed));
83
call.perform();
84
} else if (is_on && !id(auto_writing)) {
85
id(last_fan_speed) = spd;
86
}
87
id(fan_was_on) = is_on;
88
89
sensor:
90
- platform: homeassistant
91
id: playroom_co2
92
entity_id: sensor.0x00158d0008f3a21b_co2
93
internal: true
94
95
- platform: homeassistant
96
id: playroom_voc
97
entity_id: sensor.0x2222222222222222_voc_index
98
internal: true
99
100
- platform: homeassistant
101
id: playroom_pm25
102
entity_id: sensor.0x2222222222222222_pm25
103
internal: true
104
105
- platform: template
106
id: tion_current_temperature_sensor
107
name: Current Temperature
108
unit_of_measurement: "°C"
109
device_class: temperature
110
state_class: measurement
111
update_interval: 30s
112
lambda: |-
113
return id(tion_climate).current_temperature;
114
115
- platform: template
116
id: tion_target_temperature_sensor
117
name: Target Temperature
118
unit_of_measurement: "°C"
119
device_class: temperature
120
state_class: measurement
121
update_interval: 30s
122
lambda: |-
123
return id(tion_climate).target_temperature;
124
125
- platform: tion
126
id: tion_outdoor_temperature
127
type: outdoor_temperature
128
name: Outdoor Temperature
129
130
- platform: tion
131
id: tion_heater_power
132
type: heater_power
133
name: Heater Power
134
135
- platform: tion
136
id: tion_productivity
137
type: productivity
138
name: Productivity
139
140
- platform: tion
141
id: tion_filter_time_left_days
142
type: filter_time_left_days
143
name: Filter Time Left
144
145
- platform: tion
146
id: tion_boost_time_left
147
type: boost_time_left
148
name: Boost Time Left
149
150
- platform: tion
151
id: tion_fan_speed
152
type: fan_speed
153
name: Fan Speed
154
155
switch:
156
- platform: tion
157
id: tion_buzzer
158
type: sound
159
name: Buzzer
160
161
- platform: tion
162
id: tion_boost
163
type: boost
164
name: Boost
165
166
- platform: template
167
id: auto_air_switch
168
name: "Auto Mode (CO2, VOC & PM2.5)"
169
icon: mdi:fan-auto
170
optimistic: true
171
lambda: |-
172
return id(auto_air_enabled);
173
turn_on_action:
174
- lambda: 'id(auto_air_enabled) = true;'
175
turn_off_action:
176
- lambda: 'id(auto_air_enabled) = false;'
177
178
number:
179
- platform: tion
180
id: tion_boost_time
181
type: boost_time
182
name: Boost Time
183
initial_value: 20min
184
restore_value: true
185
186
- platform: tion
187
id: tion_auto_min_fan_speed
188
type: auto_min_fan_speed
189
name: Auto Min Fan Speed
190
initial_value: 1
191
restore_value: true
192
193
- platform: tion
194
id: tion_auto_max_fan_speed
195
type: auto_max_fan_speed
196
name: Auto Max Fan Speed
197
initial_value: 6
198
restore_value: true
199
200
select:
201
- platform: tion
202
id: tion_air_intake
203
type: air_intake
204
name: Air Intake
205
206
binary_sensor:
207
- platform: tion
208
id: tion_state_problem
209
type: state
210
name: State Problem
211
212
- platform: tion
213
id: tion_filter_worn_out
214
type: filter
215
name: Filter Worn Out
216
217
- platform: tion
218
id: tion_heater_state
219
type: heater
220
name: Heater State
221
222
- platform: tion
223
id: tion_power_state
224
type: power
225
name: Power State
226
227
text_sensor:
228
- platform: tion
229
id: tion_firmware_version
230
type: firmware_version
231
name: Version
232
233
- platform: tion
234
id: tion_errors
235
type: errors
236
name: Errors
237
238
- platform: template
239
id: auto_reason_sensor
240
name: "Auto Reason"
241
icon: mdi:information-outline
242
243
button:
244
- platform: tion
245
id: tion_reset_filter
246
type: reset_filter
247
name: Reset Filter
248
confirm:
249
name: Reset Filter Confirm
250
251
globals:
252
- id: auto_air_enabled
253
type: bool
254
restore_value: true
255
initial_value: 'true'
256
257
- id: last_fan_speed
258
type: int
259
restore_value: true
260
initial_value: '2'
261
262
- id: fan_was_on
263
type: bool
264
restore_value: false
265
initial_value: 'false'
266
267
- id: auto_writing
268
type: bool
269
restore_value: false
270
initial_value: 'false'
271
272
- id: correcting_fan_speed
273
type: bool
274
restore_value: false
275
initial_value: 'false'
276
277
interval:
278
- interval: 60s
279
then:
280
- if:
281
condition:
282
- binary_sensor.is_on: tion_power_state
283
- lambda: 'return id(tion_air_intake).state != "outdoor";'
284
then:
285
- select.set:
286
id: tion_air_intake
287
option: "outdoor"
288
289
- interval: 30s
290
then:
291
- lambda: |-
292
if (!id(auto_air_enabled)) {
293
id(auto_reason_sensor).publish_state("Manual");
294
return;
295
}
296
if (!id(tion_power_state).state) {
297
id(auto_reason_sensor).publish_state("Off");
298
return;
299
}
300
float co2 = id(playroom_co2).state;
301
int speed_co2;
302
if (co2 > 1600) speed_co2 = 6;
303
else if (co2 > 1400) speed_co2 = 5;
304
else if (co2 > 1200) speed_co2 = 4;
305
else if (co2 > 1000) speed_co2 = 3;
306
else if (co2 > 800) speed_co2 = 2;
307
else speed_co2 = 1;
308
float voc = id(playroom_voc).state;
309
int speed_voc;
310
if (voc > 200) speed_voc = 6;
311
else if (voc > 150) speed_voc = 5;
312
else if (voc > 120) speed_voc = 4;
313
else if (voc > 100) speed_voc = 3;
314
else if (voc > 70) speed_voc = 2;
315
else speed_voc = 1;
316
float pm25 = id(playroom_pm25).state;
317
int speed_pm25;
318
if (pm25 > 150) speed_pm25 = 6;
319
else if (pm25 > 55) speed_pm25 = 5;
320
else if (pm25 > 35) speed_pm25 = 4;
321
else if (pm25 > 20) speed_pm25 = 3;
322
else if (pm25 > 12) speed_pm25 = 2;
323
else speed_pm25 = 1;
324
int final_speed = std::max({speed_co2, speed_voc, speed_pm25});
325
std::string reason = "CO2";
326
if (final_speed == speed_pm25) {
327
reason = "PM2.5";
328
} else if (final_speed == speed_voc) {
329
reason = "VOC";
330
} else {
331
reason = "CO2";
332
}
333
id(auto_reason_sensor).publish_state(reason);
334
id(auto_writing) = true;
335
auto call = id(tion_fan).turn_on();
336
call.set_speed(final_speed);
337
call.perform();
338
id(auto_writing) = false;
P.S.: Profit :)

Discussion (0)

No comments yet. Be the first!

Maker

Avatar
Anton Shagaev
Tashkent, UZ

Anton is the Founding Engineer at Tinkster. He translates industrial reliability into software architecture, ensuring the platform's core is built to last. Anton studied oil and gas engineering in the United States and also holds two honors degrees from Tomsk Polytechnic University.

AI Project Assistant

Tinkster Neural Core

Hi! I am the AI assistant for this project. Ask me any questions about the assembly, code, or components.

Downloads

NodeMCU ESP8266 enclosure
1.5 MB