ESP32 Controlled Thermostat

ESP32 Controlled Thermostat
Legacy Thermostat actuated by ESP32 Based Device

Late Last year as the weather began to cool off, I decided I needed a better way to automate control of the heat within our New York City apartment. I wanted to do this in a renter friendly way but the challenge was that we have a very old Honeywell thermostat on the wall. Enter the idea for a device that would push and pull the lever on the thermostat using a servo controlled by ESPHome.

BOM

1 - ESP32-S3
2 - Momentary Buttons
1 - SSD1106 1.3" OLED Screen
1 - DHT22 Temperature / Humidity Sensor
1 - Tiny Roller Bearing
5 - M3 x 8mm SHCS
3 - M4 x 12mm SHCS
1 - Assortment of 24AWG wire
1 - 3 Part 3D Printed Case

Design Overview

The primary goal of this project was to design a smart device that could live permanently on the wall in our entry way to control the the legacy thermostat. This meant that I wanted the design to be fully contained and aesthetically attractive.

To achieve the functionality while making the assembly easy I went with a 3 layer stack. The top layer holds the SSD1106 OLED screen, the middle layer houses the servo, the DHT22, and the momentary buttons, and the back layer holds the ESP32-S3. The stack is then secured together with a few socket head cap screws from the back side to provide clean aesthetic from the front.

Assembly

I started by soldering leads to the screen then installing the screen to the front section of the case. The case has small pins to align the screen to the opening then the screen is secured with a few drops of CA glue.

Wires Soldered to the SSD1106
SSD1106 Mounted to Case

Next, the servo arm is connected to the servo then that assembly is mounted to the middle section of the case. The DHT22 also goes is press fit into the cutout on the side of the case at this time.

Servo and DHT22 Installed

Next, I installed the momentary buttons. Then connected the front section of the case to the middle and routed the wires for the SSD1106 through

Front and Middle Section Mated and Wires Routed

At this point I was realizing that there were going to be a lot more wires in a smaller space than I had originally pictured. However, with some creative folds and routing I was able to make it fit.

Wires Fully Soldered and Ready for Testing

Finally, I installed a stand-in push rod and was ready to configure the software and start testing.

Push Rod Installed and Ready to Be Sealed for Testing

Wiring

Screen:
GND
VCC - 3.3V
SCL - GPIO09 - White
SDA - GPIO08 - Green

Servo:
GND
VCC - 5V
PWM - GPIO02

Temp Sensor:
GND
VCC - 3.3V-6V
Signal - GPIO01

Button 1:
GND
Signal - GPIO16

Button 2
GND
Signal - GPIO17

Software

As a long time Home Assistant user many of my projects run on ESPHome due to the fantastic integration and shear flexibility of the system.

Flashing the ESP32 with ESPHome as become so easy with their web based flashing tool.

After a few iterations of figuring out what information I wanted on the screen and how to lay it out I ended up with the ESPHome YAML configuration below.

esphome:
  name: thermostat-actuator
  friendly_name: thermostat-actuator

esp32:
  board: esp32-s3-devkitc-1
  framework:
    type: arduino

# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: "KEY"

  services:
  - service: control_servo
    variables:
      level: float
    then:
      - servo.write:
          id: servo0
          level: !lambda 'return level / 100.0;'

ota:
  - platform: esphome
    password: "PASSWORD"

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Thermostat-Actuator"
    password: "PASSWORD"

captive_portal:

binary_sensor:
  - platform: gpio
    name: 'Button Up'
    pin:
      number: GPIO18
      mode: INPUT_PULLUP
    filters: 
      - invert: 
          
  - platform: gpio
    name: 'Button Down'
    pin:
      number: GPIO17
      mode: INPUT_PULLUP
    filters: 
      - invert: 
         
i2c:
  sda: GPIO9
  scl: GPIO08

display:
  - platform: ssd1306_i2c
    model: 'SH1106 128x64'
    rotation: 0
    lambda: |-
      if (id(screen).state) {

        it.printf(64, 32, id(roboto_32), TextAlign::CENTER, "%.0f°", id(average_temp).state);
        it.printf(64,56, id(roboto_12), TextAlign::CENTER, "Set: %.0f°", id(radiator_set_point).state);
      }
      else {
        it.fill(COLOR_OFF);
      }

font:
  - file:
      type: gfonts
      family: Roboto
      weight: 500
    id: roboto_32
    size: 32

  - file:
      type: gfonts
      family: Roboto
      weight: 100
    id: roboto_12
    size: 12

sensor:
  - platform: dht
    pin: GPIO01
    temperature:
      name: 'Entry Temperature'
      id: entry_temp
    humidity:
      name: 'Entry Humidity'
      id: entry_hum
    update_interval: 60s

  - platform: homeassistant
    entity_id: input_boolean.thermostat_screen
    id: screen

  - platform: homeassistant
    entity_id: input_number.radiator_temperature
    id: radiator_set_point

  - platform: homeassistant
    entity_id: sensor.average_house_temperature
    id: average_temp

servo:
  - id: servo0
    output: pwm_output
    auto_detach_time: 5s
    min_level: 3%
    max_level: 12.0%

# On ESP32, use ledc output
output:
  - platform: ledc
    id: pwm_output
    pin: GPIO02
    frequency: 50 Hz

number:
  - platform: template
    id: servoControl
    name: Servo Control
    min_value: -90
    initial_value: 0
    max_value: 90
    step: .5
    optimistic: true
    set_action:
      then:
        - servo.write:
            id: servo0
            level: !lambda 'return x / 100.0;'

The other half of the software puzzle for this device was Node Red. Below is an overview of the flows used on the Home Assistant server.

Testing

To my shock this was one of those projects that just kind of worked on the first try. After flashing the device with ESPHome and adding it to my Home Assistant server I was able to see the buttons and sensors and I was able to control the servo.

0:00
/0:08

One thing I thought was going to be difficult to overcome was that the thermostat has a pivot arm that is about 75mm and the servo is using a Scotch Yoke which has an output of a sine wave. So I thought it was going to be difficult to find an equation that would accurately output servo positions based on desired temperatures.

After gathering a handful of data points and spending more time than I would like to admit in excel trying to find a multiple order polynomial that would fit the points; I was able to just use a simple linear equation. This was then used to create the code the for the Map Temp to Servo Range node seen the Node Red screenshot above. That code can be seen below.

var temp = msg.payload;

var mapped_value = 0.0;
var offset = 2;

mapped_value = Math.round(5.9*(temp-offset)-412);

msg.payload = mapped_value;

return msg;

This takes the temperature in from an input_number helper, applies an offset, and maps that to a servo position based on the equation found earlier.

Functionality

I believe that most smart devices should still retain some manner of physical control. Finding your phone to open an app to change the temperature is just too much overhead for a device that may be used daily. Therefore, interaction with the device was left to be very simple. The top button increments the thermostat set point by 1°F and the lower button decrements it by the same amount. This allowed for quick and easy changes to the set point while allowing Home Assistant to automate the function the majority of the time.

On screen I wanted things to be simple as well. In large font at the center we have the average temperature of all the sensors in the apartment. In small text below that the current set point is shown. This allows the current temperature to be read from across the room while the set point can be read when you are in front of the device to make a change.

Conclusion, Learnings, and Findings

All in all, both my partner and I are very happy with how this project turned out. After creating automations to adjust the temperature based on time of day, outside temperature, and proximity to home it was a very comfortable even during a very cold NYC winter. I was able to preheat the apartment in the afternoons while we were at work but if we went on vacation the heat would stay off to save us money.

One of my biggest takeaways from this project was that wires take up much more space than I thought they would. I had left about a 6-8mm gap in the stack to accommodate the wires but after soldering everything together it was still a tight fit to the the case to close.

Another learning moment was to not over complicate things unless you need to. I jumped straight to a multiple order polynomial for the positioning without even trying a linear fit. This created a lot of headaches and confusion that weren't needed. If I had started with the basic solution first I could always expand to more complex solutions after gaining a base understanding.

Thanks for reading and stay tuned for future projects.