Blog | About | Hire Me

Temperature-Controlled Cold Room With A Raspberry Pi Zero

by Philippe Olivier
Created 2021-08-18

1. Introduction

Under the porch of my house is a small concrete room. During the course of renovating my house, I transformed it into a cold room by heavily insulating it. I have a small heater that keeps the temperature at a constant 8°C during the winter. During the summer, however, the temperature can go as high as around 22°C, which is not ideal as I am also using this room to age wine.

I did not want to install an A/C unit for various reasons. Instead, I decided to build a system that would take the cold outside air (usually during the night) and circulate it through the room when the temperature delta between the inside and the outside of the room would allow it to be cooled. For this purpose, two pipes make a bridge between the inside of the room and the outside of the house, and an old bathroom fan is used to circulate the air. This fan is controlled by a Raspberry Pi Zero, which has a temperature sensor inside the room, and another one outside the house.

2. Hardware

This is the hardware that I ordered:

  • Raspberry Pi Zero WH
  • Power supply
  • Case
  • 64GB SD card
  • 2x DHT22 temperature and humidity sensors (one plain DHT22, and one SEN0137)
  • 10k Ohm resistor
  • 5V relay module

3. Initial setup

First off, the initial setup of the RPi Zero is almost the same as described in a previous post (Raspberry Pi TV (Part 1)). The differences are that the stripped-down OS is chosen instead (Raspberry Pi OS Lite), and that the hostname is rpi-coldroom. Furthermore, since the RPi Zero does not have an ethernet port, we must manually enable a wireless connection, as described below.

3.1. Wireless connection

In the /boot partition, add a file wpa_supplicant.conf with the following information:

ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
country=CA
network={
    ssid="MyNetwork"
    psk="the_password"
    key_mgmt=WPA-PSK
}

Note: The RPi Zero W does not support 5GHz.

4. Circuit diagrams

4.1. Temperature sensors

The wiring diagram of the SEN0137 is straightforward: The red wire goes to 5V, the black wire goes to GROUND, and the green wire goes to DATA.

For the plain DHT22, holding the module in front of you, starting from the left:

  • The first pin is the power supply pin.
  • The second pin is the data pin.
  • The third pin is unused.
  • The fourth pin is the ground pin.
         |- 5V -------------------------|
         |- GND ------------------------| SEN0137
         |- GPIO 18 --------------------|
         | 
         |- 3V3 -----------------| PIN1 |
RPi Zero |                |             |
         |             10k Ohm          |
         |                |             | DHT22
         |- GPIO 19 -------------| PIN2 |
         |                       | PIN3 |
         |- GND -----------------| PIN4 |

4.2. Relay

         |- 5V --------| VCV  |       | COM |--- Fan ---| Wall socket
RPi Zero |- GND -------| GND  | Relay | NO  |-----------|
         |- GPIO 21 ---| DATA |       | NC  |

4.3. Button

RPi Zero |- 3V3 -|- 10k Ohm -|- Button -|- GPIO 23 -| RPi Zero

4.4. LED

RPi Zero |- GPIO 16 -|- LED -|- 1k Ohm -|- GND -| RPi Zero

5. Software

We need to install the DHT package for Python:

  $ sudo apt-get install libgpiod2 python3-pip
  $ pip3 install adafruit-circuitpython-dht

6. Testing the sensors

Let's test our two sensors to make sure that they are reading the temperature and humidity correctly.

  import adafruit_dht
  import board
  import time

  inside_sensor = adafruit_dht.DHT22(board.D19)
  outside_sensor = adafruit_dht.DHT22(board.D18)

  while True:
      time.sleep(5)
      try:
          inside_temperature = inside_sensor.temperature
          inside_humidity = inside_sensor.humidity
          outside_temperature = outside_sensor.temperature
          outside_humidity = outside_sensor.humidity
          print(f'Ti:{inside_temperature},To:{outside_temperature},Hi:{inside_humidity},Ho:{outside_humidity}')

      except RuntimeError as error:
          print(error.args[0])
          continue

7. Testing the relay

Let's test the relay.

  import RPi.GPIO as GPIO
  import time

  relay_gpio = 23
  GPIO.setmode(GPIO.BCM)
  GPIO.setup(relay_gpio, GPIO.OUT)

  while True:
      print ('ON')
      GPIO.output (relay_gpio, GPIO.LOW)
      time.sleep(2)
      print ('OFF')
      GPIO.output (relay_gpio, GPIO.HIGH)
      time.sleep(2)

Normally, the device connected to the relay should turn on/off every 2 seconds.

8. Main script

Let's put all of this together, along with the temperature policies we want for the cold room. Our policies are divided in two parts: operational policies, which are the regular cold room policies, and special policies, which take precedence over the operational policies.

Special policies:

  • To ensure that there is some air circulation, the fan must run at least 5 minutes every 24 hours. If the fan has been off for more than 24 hours, it is turned on for 5 minutes.
  • We want to prevent the fan from being turned on too often. If the fan is turned on, it must remain turned on for at least 1 minute.
  • We don't want the fan to run for extended periods of time. If the fan has been on for more than 60 minutes, turn it off.
  • We want to give a cooldown period for the fan. The fan must have been off for at least 30 minutes prior to being turned on.
  • To prevent potential problems, if there is no valid reading for some time, exit the script.

These special policies ensure that at the low end, the fan will run at least 5 minutes every day, and at the high end, it will run 60 minutes out of every 90 minute interval.

Operational policies:

  • If the inside temperature is less than 10°C, don't do anything.
  • If it is too humid outside, don't do anything.
  • If the outside temperature is less than the inside temperature (minus a small error factor), turn on the fan.

The complete script is:

  import adafruit_dht
  import board
  import datetime
  import os
  import RPi.GPIO as GPIO
  import time

  # In case the RPi Zero shuts down improperly.
  GPIO.cleanup()

  # Note: All times are in seconds.

  # Interval duration between probing the sensors.
  PROBE_INTERVAL = 30

  # Interval duration during which the user is allowed to press the poweroff button (when the light
  # flashes).
  POWEROFF_INTERVAL = 5

  # How long the poweroff button must be pressed before registering.
  POWEROFF_HOLD = 1

  # Maximum number of consecutive loops where no sensor reading is tolerated.
  MAX_NO_READINGS = 30

  # Minimum temperature, i.e., don't cool the cold room if the temperature is below this level.
  MIN_TEMP = 10

  # Maximum humidity, i.e., don't transfer air if the outside humidity is above this level.
  MAX_HUM = 80

  # Error factor for the sensors.
  EPSILON = 1

  # Some checks.
  assert(PROBE_INTERVAL >= POWEROFF_INTERVAL)
  assert(POWEROFF_INTERVAL >= POWEROFF_HOLD)

  GPIO.setmode(GPIO.BCM)

  # Sensors.
  inside_sensor = adafruit_dht.DHT22(board.D19)
  outside_sensor = adafruit_dht.DHT22(board.D18)

  # Relay.
  relay_gpio = 21
  GPIO.setup(relay_gpio, GPIO.OUT)

  # LED indicator.
  led_gpio = 16
  GPIO.setup(led_gpio, GPIO.OUT)

  # Button.
  button_gpio = 23
  GPIO.setup(button_gpio, GPIO.IN, GPIO.PUD_DOWN)

  def fan_off():
      GPIO.output(relay_gpio, GPIO.LOW)
      log('fan off')

  def fan_on():
      GPIO.output(relay_gpio, GPIO.HIGH)
      log('fan on')

  def fan_active() -> bool:
      return bool(GPIO.input(relay_gpio))
    
  def led_off():
      GPIO.output(led_gpio, GPIO.LOW)

  def led_on():
      GPIO.output(led_gpio, GPIO.HIGH)

  def log(line: str):
      print(line)
      with open('/home/pi/cold_room_log.txt', 'a') as f:
          f.write(line+'\n')
      f.close()
    
  def cleanup():
      fan_off()
      led_off()
      GPIO.cleanup()
      os.system('sudo shutdown now')
      exit()

  # The routine starts with the fan running for 10 seconds, then turning off.
  led_on()
  fan_off()
  fan_on()
  time.sleep(10)
  fan_off()
  latest = {'fan on': time.time(),
            'fan off': time.time()}

  consecutive_no_reads = 0
  while True:
      # When the light flashes, the user can press the button to exit.
      button_counter = 0
      for i in range(POWEROFF_INTERVAL*5):
          # If the button is held long enough, the script will end.
          if button_counter >= POWEROFF_HOLD*5:
              cleanup()
          # The LED flashes during the time when the button can be pressed.
          if i%2 == 0:
              led_off()
          if GPIO.input(button_gpio) == GPIO.HIGH:
              button_counter += 1
          else:
              button_counter = 0
          time.sleep(0.2)
          led_on()

      # Sleep for the rest of the probe interval.
      time.sleep(PROBE_INTERVAL-POWEROFF_INTERVAL)
        
      # Try to probe the sensors.
      try:
          inside_temp = inside_sensor.temperature
          inside_hum = inside_sensor.humidity
          outside_temp = outside_sensor.temperature
          outside_hum = outside_sensor.humidity
          log(f'{datetime.datetime.now().strftime("%Y-%m-%d-%H:%M:%S")},Ti:{inside_temp},To:{outside_temp},Hi:{inside_hum},Ho:{outside_hum}')
          consecutive_no_reads = 0
      except RuntimeError as error:
          log(f'{error.args[0]}')
          consecutive_no_reads += 1
          # If there are too many consecutive no readings of sensors, we assume that there is a
          # problem, and the script ends.
          if consecutive_no_reads > MAX_NO_READINGS:
              cleanup()
          continue

      time_now = time.time()
    
      #####################
      # Special policies. #
      #####################
    
      # The fan must turn on for at least 5 minutes every 24 hours.
      if time_now - latest['fan off'] >= 86400 and not fan_active():
          fan_on()
          latest['fan on'] = time.time()
          time.sleep(300)
          fan_off()
          latest['fan off'] = time.time()
          continue
    
      # If the fan is turned on, it must remain turned on for at least 1 minute.
      if time_now - latest['fan on'] <= 60 and fan_active():
          continue

      # If the fan has been on for more than 60 minutes, turn it off.
      if time_now - latest['fan on'] >= 3600 and fan_active():
          fan_off()
          latest['fan off'] = time.time()
          continue

      # The fan must have been off for at least 30 minutes prior to being turned on.
      if not fan_active() and latest['fan off'] <= 1800:
          continue

      #########################
      # Operational policies. #
      #########################

      # If the cold room is too cold, don't cool it more.
      if inside_temp < MIN_TEMP:
          if fan_active():
              fan_off()
              latest['fan off'] = time.time()
          continue
    
      # Don't transfer air from the outside if it is too humid.
      if outside_hum > MAX_HUM:
          if fan_active():
              fan_off()
              latest['fan off'] = time.time()
          continue

      # Cool the cold room.
      if inside_temp > outside_temp + EPSILON:
          if not fan_active():
              fan_on()
              latest['fan on'] = time.time()
      else:
          if fan_active():
              fan_off()
              latest['fan off'] = time.time()

  cleanup()

9. Autorun

To ensure that this script autoruns when the RPi Zero is booted, do the following. Create a new unit file /lib/systemd/system/coldroom.service with the following:

[Unit]
Description=Cold room
After=multi-user.target

[Service]
Type=idle
User=pi
Restart=no
ExecStart=/usr/bin/python3 /home/pi/coldroom.py

[Install]
WantedBy=multi-user.target

Now enable the this new service:

  $ sudo chmod 644 /lib/systemd/system/coldroom.service
  $ chmod +x /home/pi/coldroom.py
  $ sudo systemctl daemon-reload
  $ sudo systemctl enable coldroom.service
This website is generated by Emacs.