Smart Range Hood with ESPHOME and SONOFF 4CH PRO

I only had one light left in my house that was not controlled by Home Assistant (microwave, fridge and oven lights excluded) and that was the one over our range (stove). It also has a built in two speed fan, so I decided that needed to be automated too. Also a thermometer was included to create automations to automatically turn on/off the light and fan.

Here are the old switches and wiring:

I had an SONOFF 4CH PRO that seemed like a good fit for the project left over from a garage door automation on an old house.

One of the automation rules in our house is that everything has to work as you would expect a normal device to work. So I decided to replace the front switched on the range hood with SPDT momentary rockers. I ordered some new switches from DigiKey. This would allow an on and off for the light and a high/low for the fans that would act as both speed changing and on off. For the fan you would push to turn on and push the same side to turn off. This is not perfectly intuitive, but it works pretty well.

These are the new switches that I put in:

There are some custom lambda functions in the code to cover this logic and link it all into the multispeed fan component.

esphome:
  name: range-hood
  platform: ESP8266
  board: esp01_1m
  comment: "Sonoff 4CH Pro Range Hood Contoller"

<<: !include templates/common.yaml
binary_sensor:
  - platform: gpio
    pin:
      number: GPIO0
      mode:
        input: true
        pullup: true
      inverted: true
    name: "Light On Button"
    on_press:
      then:
        - light.turn_on: range_light
  - platform: gpio
    pin:
      number: GPIO9
      mode:
        input: true
        pullup: true
      inverted: true
    name: "Light Off Button"
    on_press:
      then:
        - light.turn_off: range_light
  - platform: gpio
    pin:
      number: GPIO10
      mode:
        input: true
        pullup: true
      inverted: true
    name: "Fan Low Button"
    on_press:
      then:
        - lambda: |-
            if (id(range_fan).state) {
              if (id(range_fan).speed == 1) {
              // Fan in on and speed is low, so toggle to off
              // Turn the fan off
                auto call = id(range_fan).turn_off();
                call.perform();
              } else {
                //Turn on fan and set to speed low
                auto call = id(range_fan).turn_on();
                call.set_speed(1);
                call.perform();
              }
            } else {
              // Fan is OFF, turn it on to low
              auto call = id(range_fan).turn_on();
              call.set_speed(1);
              call.perform();
            }
  - platform: gpio
    pin:
      number: GPIO14
      mode:
        input: true
        pullup: true
      inverted: true
    name: "Fan High Button"
    on_press:
      then:
        - lambda: |-
            if (id(range_fan).state) {
              if (id(range_fan).speed == 2) {
              // Fan in on and speed is 2, so toggle to off
              // Turn the fan off
                auto call = id(range_fan).turn_off();
                call.perform();
              } else {
                //Turn on fan and set to speed 2
                auto call = id(range_fan).turn_on();
                call.set_speed(2);
                call.perform();
              }
            } else {
              // Fan is OFF, turn it on to high
              auto call = id(range_fan).turn_on();
              call.set_speed(2);
              call.perform();
            }
  - platform: status
    name: "Range Fan and Light"
    

switch:
#  - platform: gpio
#    name: "Sonoff 4CH Relay 1"  #Light on/off
#    pin: GPIO12
#    id: relay1
#    internal: true
  - platform: gpio
    name: "Range Hood Relay 2" #On/Off for fan
    pin: GPIO5
    id: relay2
    internal: true
  - platform: gpio
    name: "Range Hood Relay 3"  #High/low for fan
    pin: GPIO4
    id: relay3
    internal: true
  - platform: gpio
    name: "Range Hood Relay 4"  #Not used yet
    pin: GPIO15
    id: relay4
    internal: true
    
fan:
  - platform: speed
    id: range_fan
    speed_count: 2
    output: range_fan_output
    name: "Range Fan"
    
output:
  # Register the blue LED as a dimmable output
  - platform: esp8266_pwm
    id: blue_led
    pin: GPIO13
    inverted: true
  - id: range_light_output
    platform: gpio
    pin: GPIO12
  - platform: template
    id: range_fan_output
    type: float
    write_action:
      - if:
          condition:
            lambda: return ((state == 0));
          then:
            - switch.turn_off: relay2
            - switch.turn_off: relay3
      - if:
          condition:
            lambda: return ((state > 0) && (state < 1));
          then:
            - switch.turn_on: relay2
            - switch.turn_off: relay3
      - if:
          condition:
            lambda: return ((state == 1));
          then:
            - switch.turn_on: relay2
            - switch.turn_on: relay3
light:
  - platform: monochromatic
    name: "Range Hood Blue LED"
    output: blue_led
    internal: true
  - platform: binary
    name: "Range Light"
    output: range_light_output
    id: range_light

dallas:
  - pin: GPIO2
    update_interval: 30s
sensor:
  - platform: dallas
    address: 0xFA000006C56BB528
    name: "Range Hood"

The four switches were wired on pigtails and soldered to the back side of the buttons on the SONOFF 4PRO.

I then attached the light to one relay to turn it on and off.

I then took the hot line and attached it to another relay normally open and then attached the output of that to a third relay with the NC attached to the fan low speed wire and NO attached to the high speed wire.

Finally I took and attached a dallas one wire temperature probe in a stainless steel case to GPIO2 along with a pull-up resistor to 3.3V and placed it inside the range hood.

I powered the SONOFF with 120VAC and fired it up.

Everything worked as expected until you (accidentally) double clicked one of the switches. With the SONOFF 4CH PRO this puts that channel into RF pairing mode, and it does not seem to timeout without pairing a switch, meaning you need to power cycle the box! This made that channel unresponsive to future commands, making the user interface non-functional (control through WiFi still worked)

There is apparently no way to disable this functionality with the stock firmware on the STM32. So the only solution was to rewrite all the firmware on the STM32 to make it just a dumb bit flipper that mirrored inputs onto the outputs since I was not taking advantage of any of the other features.

I whipped this up in STM32CubeIDE, forgive the hardcoded everything!

#include "main.h"

void SystemClock_Config(void);
static void MX_GPIO_Init(void);

int main(void)
{
  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();
   /* Initialize all configured peripherals */
  MX_GPIO_Init();
  /* Infinite loop */
  while (1)
  {
	    if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_3)==GPIO_PIN_SET)  //Check if button 1 pressed
		{
			HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6,GPIO_PIN_SET);          //Turn on key out 1
			HAL_GPIO_WritePin(GPIOC, GPIO_PIN_15,GPIO_PIN_RESET);          //Turn on Green LED 1
		}
		else
		{
			HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6,GPIO_PIN_RESET);          //Turn off key out 1
			//HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2,GPIO_PIN_RESET);          //Turn off Green LED 1
			HAL_GPIO_WritePin(GPIOC, GPIO_PIN_15,GPIO_PIN_SET);          //Turn off Green LED 4
		}
		if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_5)==GPIO_PIN_SET)  //Check if button 2 pressed
		{
			HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5,GPIO_PIN_SET);          //Turn on key out 2
			HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2,GPIO_PIN_RESET);          //Turn on Green LED 2
		}
		else
		{
			HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5,GPIO_PIN_RESET);          //Turn off key out 2
			HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2,GPIO_PIN_SET);          //Turn off Green LED 2
		}
		if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_6)==GPIO_PIN_SET)  //Check if button 3 pressed
		{
			HAL_GPIO_WritePin(GPIOB, GPIO_PIN_4,GPIO_PIN_SET);          //Turn on key out 3
			HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1,GPIO_PIN_RESET);          //Turn on Green LED 3
		}
		else
		{
			HAL_GPIO_WritePin(GPIOB, GPIO_PIN_4,GPIO_PIN_RESET);          //Turn off key out 3
			HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1,GPIO_PIN_SET);          //Turn off Green LED 3
		}
		if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_4)==GPIO_PIN_SET)  //Check if button 4 pressed
		{
			HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7,GPIO_PIN_SET);          //Turn on key out 4
			HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0,GPIO_PIN_RESET);          //Turn on Green LED 4
		}
		else
		{
			HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7,GPIO_PIN_RESET);          //Turn off key out 4
			HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0,GPIO_PIN_SET);          //Turn off Green LED 4
		}
	    if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_15)==GPIO_PIN_SET)  //Check if ESP relay 1 pressed
		{
			HAL_GPIO_WritePin(GPIOC, GPIO_PIN_14,GPIO_PIN_SET);          //Turn on relay out 1 C14

		}
		else
		{
			HAL_GPIO_WritePin(GPIOC, GPIO_PIN_14,GPIO_PIN_RESET);          //Turn off relay out 1
		}

	    if(HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_1)==GPIO_PIN_SET)  //Check if ESP relay 2 pressed
		{
			HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8,GPIO_PIN_SET);          //Turn on relay out 2 B8
		}
		else
		{
			HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8,GPIO_PIN_RESET);          //Turn off relay out 2
		}
	    if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_3)==GPIO_PIN_SET)  //Check if ESP relay 3 pressed
		{
			HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13,GPIO_PIN_SET);          //Turn on relay out 3 B9
		}
		else
		{
			HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13,GPIO_PIN_RESET);          //Turn off relay out 3
		}
	    if(HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_0)==GPIO_PIN_SET)  //Check if ESP relay 4 pressed
		{
			HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9,GPIO_PIN_SET);          //Turn on relay out 4 C13
		}
		else
		{
			HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9,GPIO_PIN_RESET);          //Turn off relay out 4
		}
  }
}

/**
  * @brief System Clock Configuration
  */
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  /** Initializes the RCC Oscillators according to the specified parameters
  * in the RCC_OscInitTypeDef structure.
  */
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;
  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
  RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }
  /** Initializes the CPU, AHB and APB buses clocks
  */
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_HSI;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_0) != HAL_OK)
  {
    Error_Handler();
  }
}

/**
  * @brief GPIO Initialization Function
  * @param None
  * @retval None
  */
static void MX_GPIO_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};

  /* GPIO Ports Clock Enable */
  __HAL_RCC_GPIOC_CLK_ENABLE();
  __HAL_RCC_GPIOF_CLK_ENABLE();
  __HAL_RCC_GPIOA_CLK_ENABLE();
  __HAL_RCC_GPIOB_CLK_ENABLE();

  /*Configure GPIO pin Output Level */
  HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13|GPIO_PIN_14|GPIO_PIN_15, GPIO_PIN_RESET);

  /*Configure GPIO pin Output Level */
  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET);

  /*Configure GPIO pin Output Level */
  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1|GPIO_PIN_2, GPIO_PIN_RESET);

  /*Configure GPIO pin Output Level */
  HAL_GPIO_WritePin(GPIOB, GPIO_PIN_4|GPIO_PIN_5|GPIO_PIN_6|GPIO_PIN_7, GPIO_PIN_SET);

  /*Configure GPIO pin Output Level */
  HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8|GPIO_PIN_9, GPIO_PIN_RESET);

  /*Configure GPIO pins : PC13 PC14 PC15 */
  GPIO_InitStruct.Pin = GPIO_PIN_13|GPIO_PIN_14|GPIO_PIN_15;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);

  /*Configure GPIO pins : PF0 PF1 */
  GPIO_InitStruct.Pin = GPIO_PIN_0|GPIO_PIN_1;
  GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  HAL_GPIO_Init(GPIOF, &GPIO_InitStruct);

  /*Configure GPIO pins : PA0 PA1 PA2 */
  GPIO_InitStruct.Pin = GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

  /*Configure GPIO pins : PA3 PA5 PA15 */
  GPIO_InitStruct.Pin = GPIO_PIN_3|GPIO_PIN_5|GPIO_PIN_15;
  GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

  /*Configure GPIO pins : PA4 PA6 */
  GPIO_InitStruct.Pin = GPIO_PIN_4|GPIO_PIN_6;
  GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
  GPIO_InitStruct.Pull = GPIO_PULLUP;
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

  /*Configure GPIO pin : PB3 */
  GPIO_InitStruct.Pin = GPIO_PIN_3;
  GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

  /*Configure GPIO pins : PB4 PB5 PB6 PB7 */
  GPIO_InitStruct.Pin = GPIO_PIN_4|GPIO_PIN_5|GPIO_PIN_6|GPIO_PIN_7;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
  GPIO_InitStruct.Pull = GPIO_PULLUP;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

  /*Configure GPIO pins : PB8 PB9 */
  GPIO_InitStruct.Pin = GPIO_PIN_8|GPIO_PIN_9;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

}

I then programmed it to the STM32 and no more issues with the double click! I also get status lights for the button presses now, not just the relays which is nice since they are not one to one mapped.

I think the whole thing came out looking very clean and functional and now all of my lights are controlled!

7 Likes

Very nice, thanks for sharing! I have a 4-speed fan and 2-level light, both on dial knobs, so I was looking for some ideas on what’s possible.

1 Like

I thought that this project was a bit over the top before I even got to this gem. :laughing:

Very cool project, thank you for sharing!

1 Like