I recently acquired a duct booster fan to exhaust my coat closet / server closet. It came with a Tuya compatible module but there wasn’t any support out there for it so I ended up replacing the WBR3 with a ESP12F and ESPHOME firmware. I didn’t add support for all the features this thing comes with because I deemed them unnecessary. I added the PID component for temperature control because I wasn’t satisfied with the bang bang controller built into the unit. This firmware allows me to constantly adjust the set point so that it’s +5ºF above the temperature outside the closet.
If anyone else out there has this fan and wants to integrate it into Home Assistant this firmware config could be useful. I’ve left off some of the basics here, but if you just create a new project in ESPHOME builder it will add that stuff for you automatically.
captive_portal:
uart:
rx_pin: GPIO3
tx_pin: GPIO1
baud_rate: 9600
time:
- platform: homeassistant
id: my_time
# Register the Tuya MCU connection
tuya:
time_id: my_time
id: tuya_mcu
on_datapoint_update:
- sensor_datapoint: 16
datapoint_type: uint
then:
lambda: !lambda |-
int16_t min_temp_local = (int16_t)(0xFFFF & x);
int16_t max_temp_local = (int16_t)(0xFFFF & (x >> 16));
if ((float)min_temp_local/10.0f != id(min_temp_store).state)
id(min_temp_store).publish_state(min_temp_local/10.0f);
if ((float)max_temp_local/10.0f != id(max_temp_store).state)
id(max_temp_store).publish_state(max_temp_local/10.0f);
- sensor_datapoint: 101
datapoint_type: raw
then:
lambda: !lambda |-
if(id(manual_fan_speed_store).state != x[0])
id(manual_fan_speed_store).publish_state(x[0]);
- sensor_datapoint: 102
datapoint_type: uint
then:
lambda: !lambda |-
uint16_t min_humid_local = (uint16_t)(0xFFFF & x);
uint16_t max_humid_local = (uint16_t)(0xFFFF & (x >> 16));
if ((float)min_humid_local/10.0f != id(min_humid_store).state)
id(min_humid_store).publish_state(min_humid_local/10.0f);
if ((float)max_humid_local/10.0f != id(max_humid_store).state)
id(max_humid_store).publish_state(max_humid_local/10.0f);
# Byte0 indicates the working mode (Bit7~BIT4 indicates the mode), Bit3 indicates whether the fan is running (0 indicates off, 1 indicates running); Bit2 indicates the temperature scale\\n
# Byte1~2 indicates the current temperature value ℃,
# Byte3~4 indicates the current temperature value ℉
# Byte5~6 indicates the current humidity
# BYTE7: Wind speed\\n
# BYTE8~9: High temperature preset value
# BYTE10~11: Low temperature preset value
# BYTE12~13: High humidity preset value
# BYTE14~15: Low humidity preset value
# BYTE16: Alarm status
# BYTE17: Alarm upload flag (0: not upload; 1: upload)
# BYTE18~19: Fan running time
- sensor_datapoint: 132
datapoint_type: raw
then:
lambda: !lambda |-
uint8_t mode = x[0] >> 4;
switch(mode) {
case 0:
id(operating_mode).publish_state("Auto");
break;
case 1:
id(operating_mode).publish_state("Manual");
break;
case 2:
id(operating_mode).publish_state("Timer");
break;
}
uint8_t fan_running = (x[0] & 0b1000) > 0;
id(fan_status).publish_state(fan_running);
uint8_t temp_scale = (x[0] & 0b100) > 0;
if(temp_scale) id(temp_unit).publish_state("F");
else id(temp_unit).publish_state("C");
uint16_t curr_temp_c_int = x[1] * 256;
curr_temp_c_int += x[2];
float curr_temp_c = (int16_t)curr_temp_c_int;
id(temp_c).publish_state(curr_temp_c/10.0f);
uint16_t curr_temp_f_int = x[3] * 256;
curr_temp_f_int += x[4];
float curr_temp_f = (int16_t)curr_temp_f_int;
id(temp_f).publish_state(curr_temp_f/10.0f);
uint16_t curr_humid_int = x[5] * 256;
curr_humid_int += x[6];
float curr_humid = (int16_t)curr_humid_int;
id(humid).publish_state(curr_humid/10.0f);
uint8_t fan_speed = x[7];
id(curr_fan_speed).publish_state(fan_speed);
text_sensor:
- platform: template
name: "Operating Mode"
id: operating_mode
- platform: template
name: "Temperature Units"
id: temp_unit
binary_sensor:
- platform: template
name: "Fan Running"
id: fan_status
sensor:
- platform: template
name: "Temperature °C"
unit_of_measurement: "°C"
icon: "mdi:thermometer"
device_class: temperature
state_class: measurement
id: temp_c
- platform: template
name: "Temperature °F"
unit_of_measurement: "°F"
icon: "mdi:thermometer"
device_class: temperature
state_class: measurement
id: temp_f
- platform: template
name: "Humidity"
unit_of_measurement: "%"
icon: "mdi:water-percent"
device_class: humidity
state_class: measurement
id: humid
- platform: template
name: "Fan Speed"
icon: "mdi:speedometer"
state_class: measurement
id: curr_fan_speed
- platform: pid
name: "PID Climate Result"
type: RESULT
- platform: pid
name: "PID Climate Error"
type: ERROR
- platform: pid
name: "PID Climate Proportional"
type: PROPORTIONAL
- platform: pid
name: "PID Climate Integral"
type: INTEGRAL
- platform: pid
name: "PID Climate Derivative"
type: DERIVATIVE
- platform: pid
name: "PID Climate KP"
type: KP
- platform: pid
name: "PID Climate KI"
type: KI
- platform: pid
name: "PID Climate KD"
type: KD
# Power switch
switch:
- platform: "tuya"
name: "Power"
switch_datapoint: 1
inverted: True
- platform: "tuya"
name: "Child Lock"
switch_datapoint: 107
- platform: "tuya"
name: "Query Device"
switch_datapoint: 109
- platform: template
id: max_temp_disable
name: "Disable Max Temperature"
lambda: !lambda |-
return (id(max_temp_store).state == 0x7FFF);
turn_on_action:
then:
lambda: !lambda |-
id(max_temp_store).publish_state(0x7FFF);
turn_off_action:
then:
lambda: !lambda |-
id(max_temp_store).publish_state(750);
- platform: template
id: min_temp_disable
name: "Disable Min Temperature"
lambda: !lambda |-
return (id(min_temp_store).state == 0x7FFF);
turn_on_action:
then:
lambda: !lambda |-
id(min_temp_store).publish_state(0x7FFF);
turn_off_action:
then:
lambda: !lambda |-
id(min_temp_store).publish_state(350);
- platform: template
id: max_humid_disable
name: "Disable Max Humidity"
lambda: !lambda |-
return (id(max_humid_store).state == 0x7FFF);
turn_on_action:
then:
lambda: !lambda |-
id(max_humid_store).publish_state(0x7FFF);
turn_off_action:
then:
lambda: !lambda |-
id(max_humid_store).publish_state(600);
- platform: template
id: min_humid_disable
name: "Disable Min Humidity"
lambda: !lambda |-
return (id(min_humid_store).state == 0x7FFF);
turn_on_action:
then:
lambda: !lambda |-
id(min_humid_store).publish_state(0x7FFF);
turn_off_action:
then:
lambda: !lambda |-
id(min_humid_store).publish_state(500);
# Operating Mode
select:
- platform: "tuya"
name: "Operating Mode"
enum_datapoint: 2
options:
0: Automatic
1: Manual
2: Timer
- platform: "tuya"
name: "Temperature Unit"
enum_datapoint: 23
options:
0: C
1: F
number:
- platform: template
min_value: -40
max_value: 0x7FFF
step: 1
id: min_temp_store
internal: True
optimistic: True
- platform: template
min_value: -40
max_value: 0x7FFF
step: 1
id: max_temp_store
internal: True
optimistic: True
- platform: template
name: "Minimum Temperature"
min_value: -40
max_value: 212
step: 0.1
id: min_temp
icon: "mdi:thermometer"
lambda: !lambda |-
return id(min_temp_store).state;
set_action:
then:
lambda: !lambda |-
id(min_temp_store).publish_state(x);
int16_t max_temp = id(max_temp_store).state*10;
int16_t min_temp = (int16_t)x*10;
uint32_t min_max = ((int16_t)max_temp << 16) & min_temp;
id(tuya_mcu).set_integer_datapoint_value(16, min_max);
- platform: template
name: "Maximum Temperature"
min_value: -40
max_value: 212
step: 0.1
id: max_temp
icon: "mdi:thermometer"
lambda: !lambda |-
return id(max_temp_store).state;
set_action:
then:
lambda: !lambda |-
id(max_temp_store).publish_state(x);
int16_t max_temp = (int16_t)x*10;
int16_t min_temp = id(min_temp_store).state*10;
uint32_t min_max = (max_temp << 16) & min_temp;
id(tuya_mcu).set_integer_datapoint_value(16, min_max);
- platform: template
min_value: 0
max_value: 0x7FFF
step: 1
id: min_humid_store
internal: True
optimistic: True
- platform: template
min_value: 0
max_value: 0x7FFF
step: 1
id: max_humid_store
internal: True
optimistic: True
- platform: template
name: "Minimum Humidity"
min_value: 0
max_value: 100
step: 0.1
id: min_humid
icon: "mdi:water-percent"
lambda: !lambda |-
return id(min_humid_store).state;
set_action:
then:
lambda: !lambda |-
id(min_humid_store).publish_state(x);
uint16_t max_humid = id(max_humid_store).state*10;
uint16_t min_humid = (uint16_t)x*10;
uint32_t min_max = ((uint16_t)max_humid << 16) & min_humid;
id(tuya_mcu).set_integer_datapoint_value(16, min_max);
- platform: template
name: "Maximum Humidity"
min_value: 0
max_value: 100
step: 0.1
id: max_humid
icon: "mdi:water-percent"
lambda: !lambda |-
return id(max_humid_store).state;
set_action:
then:
lambda: !lambda |-
id(max_humid_store).publish_state(x);
uint16_t max_humid = (uint16_t)x*10;
uint16_t min_humid = id(min_humid_store).state*10;
uint32_t min_max = (max_humid << 16) & min_humid;
id(tuya_mcu).set_integer_datapoint_value(16, min_max);
- platform: "tuya"
name: "Backlight Brightness"
number_datapoint: 44
min_value: 0
max_value: 100
step: 1
- platform: template
min_value: 0
max_value: 0x7FFF
step: 1
id: manual_fan_speed_store
internal: True
optimistic: True
- platform: template
name: "Manual Fan Speed"
min_value: 0
max_value: 10
step: 1
id: manual_fan_speed
icon: "mdi:fan"
lambda: !lambda |-
return id(manual_fan_speed_store).state;
set_action:
then:
lambda: !lambda |-
id(manual_fan_speed_store).publish_state(x);
id(tuya_mcu).set_raw_datapoint_value(101, {(uint8_t)x, 0, 0, 0, 0});
- platform: "tuya"
name: "Auto Fan On Speed"
number_datapoint: 103
min_value: 0
max_value: 10
step: 1
- platform: "tuya"
name: "Auto Fan Off Speed"
number_datapoint: 104
min_value: 0
max_value: 10
step: 1
output:
- platform: template
type: float
id: fan_speed_scaled
min_power: 0.1
max_power: 1
write_action:
then:
lambda: !lambda |-
id(manual_fan_speed).publish_state((uint8_t)(state*10+0.5));
id(tuya_mcu).set_raw_datapoint_value(101, {(uint8_t)(state*10+0.5), 0, 0, 0, 0});
climate:
- platform: pid
id: pid_climate
name: "PID Climate Controller"
sensor: temp_c
default_target_temperature: 23.3°C
cool_output: fan_speed_scaled
control_parameters:
kp: 0.76394
ki: 0.00219
kd: 66.59221
deadband_parameters:
threshold_high: 0.5°C
threshold_low: -1.0°C
kp_multiplier: 0.5
ki_multiplier: 0.5
kd_multiplier: 0.5
deadband_output_averaging_samples: 5
# button:
# - platform: template
# name: "PID Climate Autotune"
# on_press:
# - climate.pid.autotune: pid_climate
# interval:
# - interval: 5s
# then:
# lambda: !lambda |-
# id(tuya_mcu).set_boolean_datapoint_value(109, true);