Arduino,  Hardware,  Software

Making a cheap pedestal fan smarter

What more can you ask from a fan? They give us air in hot summers, and they don’t ask for much in return… Compared to the typical air conditioner, which consumes hundreds of watts, even more than a thousand, a pedestal fan is happy with less than 100, usually . Does it make sense to make them smart? Does it at least make more sense than the fact that I am writing this in the middle of January, when this same fan is already stored until summer comes again?

As they say, “everything has already been invented”… There are already fan models with Wi-Fi that can be connected to your home automation system. Simple, you could pay from 70 to 100 euros and boom, you would already have a smart fan. But that’s not how things are done here at El Programador Ibérico 😏.

Let’s start by disassembling the fan and seeing how it works. The front button panel has one input wire (which comes from the live wire) and three output wires, and when each of the buttons is pressed, the circuit between the input cable and one of the output cables is closed, leaving the rest unconnected. We will have to replicate this same operation: receive an order from the home automation server and activate one of 3 outputs, depending on the speed we want, deactivating the others.

To connect the live wire with the different outputs, which each go to a motor winding, we will use basic relays. In my case, 5v models, which can connect circuits that run up to 10A at 250v in AC (alternating current):

To power these relays and take that load off the microcontroller, we will use some transistors, in this case the classic 2N2222 (NPN) model. To protect these transistors from the voltage spike that occurs when the relays stop being powered (you can read more here), we will use 1N4148 diodes. And finally, in order not to leave the buttons that came with the fan unused… why not also connect them to the microcontroller, through a 10Kohm resistor, and be able to continue using them? This way the fan will not lose any functionality, it will only gain it 😊.

As a microcontroller we can use a… What? Another ESP-01? You say I am repeating myself with so many projects using the same 8-pin microcontroller? Hey, you can’t tell me it’s not useful, as small as it is 😊. No, in this case, since we are going to make use of the 3 buttons of the fan, in addition to the 3 outputs for the motor, the 4 GPIO pins of the little ESP-01 are not enough. How about a NodeMCU, for a change? (Or one of its Chinese substitutes).

The assembly that we are going to do could be summarized in the following diagram: three input buttons, which through a resistor connect their corresponding microcontroller pin to ground when they are pressed, three output pins, which when activated send a HIGH signal to the base of its corresponding transistor, which connect the ground of its corresponding relay, the live wire, connected to the center pin of the three relays, and the three motor wires, each one connected to the normally open pin of its corresponding relay.

As I have already repeated a lot with that “in the future I will make proper PCB boards, instead of using perfboards”, I think it is time to design a proper PCB for this project 😊. The truth is this was the first time for me, and it was quite difficult for me to place the different components in such a way that the traces weren’t too complex, even though the number of components was small 😫. It ended up looking like this:

What a weird shape! Why is it not square? Why does it have two holes in the middle? Calm down, calm down. I have no idea. Just kidding 😜. The shape is adapted to the inside of the fan body: it turns out that in the place where the PCB could be mounted, the shape is curved, and the part that would be on top is narrower than the one below, so to take advantage of all the space possible I made the PCB this way. The two holes are there because the manufacturer left a couple of screw posts on the inside of the fan, I guess for another model. It should serve great as a place to screw the PCB, so I took advantage of it 😊.

Now time to turn the design into a real PCB. I could do it by hand, with transparencies, photosensitive copper PCBs, an insolator, ferric acid… But this time I decided to try one of the PCB services that exist. In my case, JLCPCB. The prices are quite good. If you don’t need express delivery, you can have 5 PCBs for €10, shipping included. And how about the result? They seem quite good to me:

Let’s solder this thing! The same afternoon of the day they arrived I started to assemble them. I just couldn’t wait! 😊. Like everything in life, things don’t turn out the way we want the first time… so I noticed I made a couple of mistakes when choosing which pins to use for the input buttons: I used pins D3 and D4 for two of the buttons, which correspond to GPIOs 0 and 2 of the ESP8266, which are reserved for choosing the boot mode of the microcontroller. They could be used, with some limitations, but I didn’t take them into account, so I ended up using a couple of small wires to connect those buttons to pins D5 and D6, which can be used without limitations. A while later, I already had this:

To power this, I used another one of my old cell phone chargers. In this case, since the microcontroller can be powered without problem at 5V, I didn’t have to do anything to lower the input voltage 😊. Some last soldering and… everything mounted inside the fan!

The software for this project is similar to that of my two air conditioners (johnson and daitsu): An MQTT client, which connects to my home automation server (which I recently changed to Home Assistant), an infinite loop, which publishes each X seconds to the server about the state of operation, and in this case, checks if there have been changes in the state of the buttons, to react and change said state. In the setup() function, we will make the preparations, preparing the pins that are going to be used for inputs (the fan buttons), and outputs (the three relays of the different speeds).

void setup() {
  Serial.begin(115200);
  
  pinMode(speed1ButtonPin, INPUT_PULLUP);
  pinMode(speed2ButtonPin, INPUT_PULLUP);
  pinMode(speed3ButtonPin, INPUT_PULLUP);
  pinMode(speed1RelayPin, OUTPUT);
  pinMode(speed2RelayPin, OUTPUT);
  pinMode(speed3RelayPin, OUTPUT);

  setup_wifi();
  client.setServer(mqttServer, mqttPort);
  client.setCallback(callback);
}

The callback function (the one that will be called when we receive commands via MQTT) has a function to convert the value that is received in the payload to a byte number (0 for off, 1, 2 or 3 for the speeds) and convert that into off-on states, which are stored in a byte value (using the last 3 bits, that is, the least significant, and where 1 = ON and 0 = OFF). In this way a speed command of 1 will be converted to a value 0b00000001 (last bit with value 1), and a speed command of 3 to a value 0b00000100 (third least significant bit to 1, the rest to 0). This value is sent to another function, which is the one that changes the states of the relays:

void callback(char* topic, byte* payload, unsigned int length) {
  String topicString = String(topic);
  byte* payloadZeroTerm = (byte*)malloc(length+1);
  String payloadString;
  byte payloadByte;

// Conversion of payload to String
  memcpy(payloadZeroTerm, payload, length);
  payloadZeroTerm[length] = '\0';
  payloadString = String((char*)payloadZeroTerm);
  Serial.print("Received message from topic ");
  Serial.print(topic);
  Serial.print(": ");
  Serial.println(payloadString);

// Conversion of payload to Byte
  payloadByte = (byte)payloadString.toInt();

// Speed Topic: Payload will be "0", "1", "2", or "3"
  if(topicString.equals(String(mqttSpeedCommandTopic))) {
    if (payloadByte == 0) {
      updateRelayState(0b00000000);  // All OFF
    } else if (payloadByte == 1) {
      updateRelayState(0b00000001);  // Speed 1
    } else if (payloadByte == 2) {
      updateRelayState(0b00000010);  // Speed 2
    } else if (payloadByte == 3) {
      updateRelayState(0b00000100);  // Speed 3
    }
  }

  publishStates();
}

In the updateRelayState() function, we receive the states of the relays by parameter, and the function acts to turn off the all outputs and turn on the one that must be turned on. I created a small delay between the deactivation of the current relays and the activation of the new one, to give the relay that was activated time to return to its disconnect position, to minimize problems (so that there is no more than one coil of the motor powered at any time). This supposed case could cause some problem, since there would be more than one coil “competing” to move the fan blades at different speeds. We better avoid that 😊.

void updateRelayState(byte relayState) {
  if (relayState & 0b00000100) {  // Speed 3
    digitalWrite(speed1RelayPin, LOW);
    digitalWrite(speed2RelayPin, LOW);
    if (currentSpeed > 0) {
      delay(100);
    }
    digitalWrite(speed3RelayPin, HIGH);
    currentSpeed = 3;
  } else if (relayState & 0b00000010) {  // Speed 2
    digitalWrite(speed1RelayPin, LOW);
    digitalWrite(speed3RelayPin, LOW);
    if (currentSpeed > 0) {
      delay(100);
    }
    digitalWrite(speed2RelayPin, HIGH);
    currentSpeed = 2;
  } else if (relayState & 0b00000001) {  // Speed 1
    digitalWrite(speed2RelayPin, LOW);
    digitalWrite(speed3RelayPin, LOW);
    if (currentSpeed > 0) {
      delay(100);
    }
    digitalWrite(speed1RelayPin, HIGH);
    currentSpeed = 1;
  } else {  // Speed 0
    digitalWrite(speed1RelayPin, LOW);
    digitalWrite(speed2RelayPin, LOW);
    digitalWrite(speed3RelayPin, LOW);
    currentSpeed = 0;
  }
}

Four times per second the fan button pins are read to see if the state has changed (a new button has been pressed). This operation is done in the getButtonState() function, and the button state values are returned in the same format as the callback() function, so that if they have changed, they are sent to updateRelayState().

byte getButtonState() {
  byte stateReturn = 0b00000000;
  
  byte buttonState = digitalRead(speed1ButtonPin);
  if (buttonState == LOW) {
    stateReturn = stateReturn | 0b00000001;
  }

  buttonState = digitalRead(speed2ButtonPin);
  if (buttonState == LOW) {
    stateReturn = stateReturn | 0b00000010;
  }
  
  buttonState = digitalRead(speed3ButtonPin);
  if (buttonState == LOW) {
    stateReturn = stateReturn | 0b00000100;
  }

  lastButtonStateTime = millis();

  return stateReturn;
}

That’s almost it. Every X seconds, the state is sent to the MQTT server, so that it is updated, through the publishStates() function. When a speed change is made, an update is sent immediately, so that it does not arrive X seconds later at the server.

void publishStates() {
//  Publish speed
  if (currentSpeed == 0) {
    client.publish(mqttSpeedStateTopic, "0");
  } else if (currentSpeed == 1) {
    client.publish(mqttSpeedStateTopic, "1");
  } else if (currentSpeed == 2) {
    client.publish(mqttSpeedStateTopic, "2");
  } else if (currentSpeed == 3) {
    client.publish(mqttSpeedStateTopic, "3");
  }
}

The configuration of the device in Home Assistant, since we are using MQTT, must be done through the configuration.yaml file. We can follow the official help page to configure it. In my case I used a select device type (this way, the speeds will appear as a dropdown list). Here we will paste the same command_topic and state_topic that we defined in “configuration.h” on the microcontroller sketch.

select:
  - platform: mqtt
    name: Ventilador dormitorio
    unique_id: select.floor1_bedroom_fan
    command_topic: Home/Floor1/Bedroom/Fan/Command
    state_topic: Home/Floor1/Bedroom/Fan/State
    options:
      - "0"
      - "1"
      - "2"
      - "3"

Down below you can download everything you would need to replicate the project, in case anyone is interested. You also can find this info on my GitHub.

See you in the next post!

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Ver
Privacidad