Programmable Thermostat with the Raspberry Pi

Required hardware:

  • Raspberry Pi model B
  • Dallas DS18B20 temperature sensor
  • Relay board
  • Push button
  • LCD display (Nokia 5110-3310)
  • Edimax WiFi nano USB adapter

Programmable Thermostat with the Raspberry Pi

Software installation

Some packages are required from the standard Raspbian repository:

apt-get install python-dev
apt-get install python-imaging

We need also PIP (a Python package installer which uses the Python Package Index repository) to install the RPi.GPIO package:

apt-get install python-pip
pip install RPi.GPIO

Then download and install the Adafruit PCD8544 Nokia 5110 LCD library:

git clone https://github.com/adafruit/Adafruit_Nokia_LCD.git
cd Adafruit_Nokia_LCD
python setup.py install

We also used PF Tempesta Seven, a free TrueType font for the LCD display.

The Nokia 5110/3310 LCD

Follow this guide to make an Hello world program with the LCD display: Nokia 5110/3310 LCD Python Library.

SNMP

We can export data (current temperature, programmed temperature and relay status) to remote systems using SNMP, in this way we can – e.g. – plot a nice temperature graph into a Cacti system. The required packages are:

apt-get install snmpd snmp snmp-mibs-downloader

Programmable Thermostat with the Raspberry Pi Schematic

The shell script /usr/local/bin/protherm-snmp reads values from the shared memory file /run/shm/protherm/stats, then we add some extend commands into /etc/snmp/snmpd.conf:

extend     temp    /usr/local/bin/protherm-snmp temp
extend     tprog   /usr/local/bin/protherm-snmp tprog
extend     switch  /usr/local/bin/protherm-snmp switch

If everything is working well, we can query the current temperature via SNMP on the command line

snmpwalk -v 2c -c public 127.0.0.1 'NET-SNMP-EXTEND-MIB::nsExtendOutput1Line."temp"'

syslog

We want to replace the file-based syslog (provided by the rsyslog package), with a memory ring-buffer logger, like the one provided by the busybox-syslogd package (this will remove rsyslog automatically):

apt-get install busybox-syslogd

The memory buffer can be read with logread, use the -f switch to see data as it arrives.

The size of the memory buffer is configured into /etc/default/busybox-syslogd, the default option is -C128, which allocates only 128 kb.

We also decreased the verbosity of cron: we want to log only errors in cron jobs, not every started job. In /etc/default/cron we added:

EXTRA_OPTS="-L 4"

Nginx Webserver

Apache does not fit well in a low-memory device like the Raspberry Pi, so we looked at lighttpd an nginx alternatives. It seems that nginx is more modern and suitable for the work. See also this Tutorial – Install Nginx and PHP on Raspbian. We want also the relative PHP module:

apt-get install nginx php5-fpm

The web interface is rather simple, it offers a snapshot of the LCD display and a graph of the temperature of the last day. It is also possible to emulate the button press, to change the program.

Hardware Test and Debug

Once assembled, we can test each piece of hardware separately.

Push Button

WARNINIG :!: Current version of ProTherm uses GPIO2 for push button. Beware that GPIO2 and GPIO3 are not intended as generic INPUT ports, see notes below.

Let’s say that the push button is attached to the GPIO #18, we need to expose the port and configure it as an input port:

echo "18" > /sys/class/gpio/export
echo "in" > /sys/class/gpio/gpio18/direction

reading the port will return 1 if button is released, zero if it is pressed (the Raspberry Pi has a default pull-up configured via software):

cat /sys/class/gpio/gpio18/value

This is a Python program which prints some text as the button is pressed:

import RPi.GPIO as GPIO
import time
GPIO.setmode(GPIO.BCM)
GPIO.setup(18, GPIO.IN, pull_up_down=GPIO.PUD_UP)
while True:
    input_state = GPIO.input(18)
    if input_state == False:
        print('Button Pressed')
        time.sleep(0.2)

Fake (erratic) button presses

Strange to say, the push button was the most challenging task of the entire project. At the first I connected the button directly to the GPIO and to the ground, the GPIO was programmed with the software PULL-UP. In this way the GPIO reads normally an HIGH state, when the button is pressed, the state becomes LOW.

With this configuration I got several fake button press, i.e. the GPIO reads a LOW state despite no-one was pressing the button. I suspected that the problem was a floating in the power supply and indeed the problem sometimes arises when the relay is switched to ON and it draws 5v from the Raspberry, may be causing a little voltage sag in the PULL-UP

Someone (see the replies to this post) states that the PULL-UP resistor activated by software is too big (may be 50 kΩ), so the PULL-UP current is too weak and voltage can drop down unexpectedly. The suggested solution is to add an external 4.7 kΩ PULL-UP resistor anyway.

I tried also to use the software PULL-DOWN resistor, in this case the push button is used to connect the GPIO to the 3.3 v. I don’t know why this mode is not so widespread as I would expect, nevertheless here it is an article using the PULL-DOWN configuration. With this configuration the false signals are worse: almost every time the relay changes state (from OFF to ON or vice-versa), the Raspberry Pi gets a false GPIO signal. So it seems that it is not a voltage sag, because the GPIO registered a fake HIGH state. May be the wire acts like an antenna, getting the 50 Hz hum.

I tried also to make a full debounce circuit. Using the GPIO18 with the internal PULL-UP, I added a 39 kΩ external resistor to enforce the PULL-UP and a resistor-capacitor pair (5.6 kΩ, 100 nF) to filter the transient noise (see the schematic here).

Finally I tried the GPIO2, which has a very strong 1.8 kΩ PULL-UP resistor in hardware (in addition to the software configurable one).

No matter what I have tried, it seems then that the software PULL-UP or PULL-DOWN is unreliable whenever you attach a wire to the GPIO header, even a short one (10 cm).

THE PROBLEM SEEMS NOW SOLVED: Because no erratic pull-down events occur if no wires are attached to the GPIO, clearly it is not a voltage sag. It seems also that no erratic behaviour occur if the boiler is detached from the relay. Everything suggests that the problem is the interference with the 220 v alternate current flowing in the cable from the boiler; keeping the wires far apart and short, seems to have solved the issue!

The PULL-UP and the safeguard restistor

Several Raspberry Pi tutorials show a push button used with the GPIO configuread as an INPUT port along with a PULL-UP resistor. The PULL-UP resistor is an external one or the internal one (software configured). In this configuration the GPIO pin is keept normally in the HIGH state (3.3 v) by the current flowing through the PULL-UP resistor.

When the push button is pressed, the GPIO is connected to the ground through another external 1 kΩ resistor. This resistor is not strictly needed, but it serves just as a safeguard if someone configures (by mistake) the GPIO as an OUTPUT; in this case the GPIO can be set HIGH (3.3 v) by software and if you press the button you will short-circuit the 3.3 v to the ground. The additional resistor will prevent any damage to the GPIO circuit.

Some GPIO pins (namely GPIO2 and GPIO3) have an hardware PULL-UP resistor in addition to the software configurable one. According to this page (see also this post and this one), the PULL-UP is a 1.8 kΩ resistor. In this case the safeguard resistor is useful also to limit the current drain when the button is pushed. Some tests revealed that a 1 kΩ resistor is not sufficient to pull down the line (it does not win over the 1.8 pull-up), a 560 Ω one is OK. Beware that using such GPIOs will draw a noticeable amount of current when the button is pressed; according to the Ohm law: 3.3 V / (1800 + 560) Ω = 1.40 mA.

Relay module

Let’s say that one relay is attached to GPIO #17, we need to expose the port and configure it as an output port:

echo "17" > /sys/class/gpio/export
echo "out" > /sys/class/gpio/gpio17/direction

then we can output value 1 or zero to that port; the pin will get 3.3 volt (high) or zero (low) respectively. Our 2 Relay Module uses an active-low logic, so write zero to activate the relay:

echo "1" > /sys/class/gpio/gpio17/value
sleep 2
echo "0" > /sys/class/gpio/gpio17/value

Temperature Sensor

See this how-to: Device Trees, Overlays and Parameters.

Older Raspbian versions do things in a different way, see this DS18B20 and 30-Jan firmware.

With Rasbian and firmware (kernel) Linux raspberrypi 4.1.7+, check that you have the following lines into /boot/config.txt:

# Uncomment to disable Device Tree (revert to the old way of doing things).
#device_tree=

# Load the w1_gpio module, the 1-Wire bus is connected to GPIO pin #4
dtoverlay=w1-gpio,gpiopin=4

With this configuration kernel modules w1_gpio and w1_therm will be loaded automatically with the right parameters. No explicit load should be required in /etc/modules.

Check also that kernel modules w1-gpio and w1-therm are not blacklisted in /etc/modprobe.d/raspi-blacklist.conf (notice that we use the SPI interface for the Nokia LCD, I2C bus is not used instead):

# We need SPI (for the Nokia LCD), blacklist only I2C
#blacklist spi-bcm2708
blacklist i2c-bcm2708

At bootsrap time the kernel will log something like this:

w1-gpio [email protected]: gpio pin 4, external pullup pin -1, parasitic power 0

Then you can load kernel modules (if not loaded automatically) and access data from the device. Notice that the device ID is unique:

modprobe w1-gpio
modprobe w1-therm
cd /sys/bus/w1/devices/
ls
28-0000067bc546  w1_bus_master1
cd 28-0000067bc546
cat w1_slave
ab 01 4b 46 7f ff 05 10 92 : crc=92 YES
ab 01 4b 46 7f ff 05 10 92 t=26687

If the sensor is not properly connected or broken, the kernel module logs some error messages, like this:

w1_master_driver w1_bus_master1: Family 0 for 00.800000000000.8c is not registered.

LCD Display

With Rasbian and firmware (kernel) Linux raspberrypi 4.1.7+, check that you have the following lines into /boot/config.txt:

#device_tree=
dtparam=spi=on

Check also that kernel modules spi-bcm2835 is not blacklisted in /etc/modprobe.d/raspi-blacklist.conf:

# blacklist spi and i2c by default (many users don't need them)
#blacklist spi-bcm2708
blacklist i2c-bcm2708

At bootsrap time the kernel will log something like this:

spi spi0.0: setting up native-CS0 as GPIO 8
spi spi0.1: setting up native-CS1 as GPIO 7

You should find two devices:

crw------- 1 root root 153, 0 Jan  1  1970 /dev/spidev0.0
crw------- 1 root root 153, 1 Jan  1  1970 /dev/spidev0.1

Running a Telegram Bot on the Raspberry Pi

To install the Telepot Python library, run as root:

apt-get install python-pip
pip install telepot

The Telepot Python library will be installed into /usr/local/lib/python2.7/dist-packages/telepot/.

You must create the new bot talking with the Telegram BotFather bot. This is an example of a Telegram chat session:

/newbot
Bot Short Name
newname_bot

The Bot Short Name will displayed by the Telegramm App into the contacts list, while the newname_bot is the actual bot name which must end with the bot string.

You will receive a message containing the HTTP API access token, a string like this one: 789463003:MDUxNTY0IDAwMDAwIG4gCjAwMDAwNTE1ODM.

Here are some useful commands to change the bot, using the BotFather:

/setuserpic Set a picture for the bot.
/setdescription Set a short description, stating “What can this bot do?”
/setname Change the short, descriptive name of the bot.

Upgrading to Telepot 9.1

We experience an annoying bug: the message handler seems to stop working after a while (some days), no more polling to the Telegram server is observed.

In the hope to resolve the problem, we upgraded the Telegram Telepot library from version 4.1 to 9.1:

pip install telepot --upgrade

The new Telepot version requires urllib3 >= 1.9.1 which ineed is installed. Neverthless pip downloaded and installed urllib3-1.18 from its repository. The version installed by pip overrides the one installed by Debian, even if you manually install a backport:

You can check the module version loaded by Python in this way:

# dpkg -i python-urllib3_1.12-1~bpo8+1_all.deb
# python
Python 2.7.9 (default, Mar  8 2015, 00:52:26) 
>>> import urllib3
>>> print urllib3.__version__
1.18

The new urllib3 module suggests also PySocks (verify with pip list), which we installed with the Debian (Jessie) package python-pysocks.

The main difference I noticed after the upgrade, was the change of two function names:

  • bot.notifyOnMessage(handle) ⇒ bot.message_loop(handle)
  • telepot.glance2(msg) ⇒ telepot.glance(msg)

Installing on Raspberry Pi OS 2021-10-30 Bullseye

The latest version of the ProTherm software should work on Debian 11 Bullseye, this means using Python 3.9 both for the ProTherm package and for the Telegram bot daemon. Also the Adafruit libraries used to drive the LCD should run on Python 3.

I copied the Raspberry Pi OS Lite image onto the SD card, before the first bootstrap I enabled remote ssh and WiFi by creating two files into the boot partitions:

  • /boot/ssh – Just create an empty file.
  • /boot/wpa_supplicant.conf – Create a suitable file with your home ESSID and PSK.

Debian Packages

The extra Debian packages required by the ProTherm software are:

  • busybox-syslogd
  • ntp
  • snmpdrrdtool
  • nginxphp-fpmphp-gdphp-imagick
  • python3-pip
  • python3-daemonpython3-lockfilepython3-netifacespython3-ntplibpython3-pil
  • python3-python-telegram-bot
  • python3-rpi.gpio
  • Required by the adafruit-circuitpython-pcd8544 library:
    • python3-serial
    • python3-usb

Operating System Customization

This is a brief check-out list of customizations required by the operating system. See above for more details.

  • Disabled the serial line console from /boot/cmdline.txt, removing console=serial0,115200.
  • Disabled login on serial console with systemctl disable [email protected].
  • Using raspi-config:
    • Interface Options ⇒ I2C ⇒ No (this will insert dtparam=i2c_arm=off into /boot/config.txt).
    • Interface Options ⇒ SPI ⇒ Yes (this will insert dtparam=spi=on into /boot/config.txt).
    • Localisation Options ⇒ Timezone ⇒ Europe/Rome
    • Localisation Options ⇒ Locale ⇒ en_US.UTF-8
  • Copied PF Tempesta Seven TTF fonts into /usr/local/share/fonts/.

CPU overload and kworker

There is a problem with the old Raspberry Pi model B and the new Debian Bullseye, with top you can view several processes takin about 10% od the CPU during idle time:

kworker/0:0+events_power_efficient
kworker/0:5-events

I turned out that an idle eth0 causes that, just disable eth0 to restore the CPU to 0.3%:

ifconfig eth0 down

To leave the interface down at bootstrap you must configure the dhcpcd daemon, which is repsponsible for network configuration in Raspberry Pi OS. Edit the file /etc/dhcpcd.conf and add the following line:

denyinterfaces eth0

Nginx and PHP

You can verify that the web server and PHP are running:

systemctl status nginx.service
systemctl status php7.4-fpm.service

Check also if the socket /run/php/php7.4-fpm.sock exists. Enable PHP by editing /etc/nginx/sites-available/default:

    index index.php index.html index.htm index.nginx-debian.html;

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        # With php-fpm (or other unix sockets):
        fastcgi_pass unix:/run/php/php7.4-fpm.sock;
    }

If you want Nginx to log to syslog instead that into a file, edit /etc/nginx/nginx.conf:

    access_log syslog:server=unix:/dev/log;
    error_log syslog:server=unix:/dev/log;

Installing the new Adafruit LCD library

WARNING: Do not use this library on the old Raspberry Pi model B, it is too CPU intensive! Use the old one.

WARNING: The new library is more CPU intensive than the old one. I opened an issue on GitHub. So I decided to give-up with the new library and use the old one (see below for installation tips). Here you can see a CPU utilization graph, the first half is using the old library, the second half is using the new one:

owdays (2021), to operate the Nokia LCD, we should use the Adafruit CircuitPython PCD8544 Python library. Detailed instructions can be found on the page Nokia 5110/3310 Monochrome LCD. In the past (2016) it was used the Adafruit Nokia LCD library, which is now phased-out.

To download the library adafruit-circuitpython-pcd8544 and its dependencies without installing them:

mkdir -p /usr/local/download/pcd8544
cd /usr/local/download/pcd8544
pip3 download adafruit-circuitpython-pcd8544

Some dependencies (libraries) exist as Debian Bullseye packages, but the version does not meet the requirements, so I uninstalled the Debian packages to install the PIP downloaded ones:

dpkg --purge python3-sysv-ipc
dpkg --purge python3-ftdi1

To avoid automatic dependency resolution (and related download/installation), I used the –no-deps option for each installation, checking manually the proper installation order (see the Requires-Dist entries into each packages METADATA):

pip3 install --no-deps adafruit_circuitpython_pcd8544-1.2.6-py3-none-any.whl
pip3 install --no-deps Adafruit_Blinka-6.18.0-py3-none-any.whl
pip3 install --no-deps adafruit_circuitpython_busdevice-5.1.1-py3-none-any.whl
pip3 install --no-deps adafruit_circuitpython_framebuf-1.4.8-py3-none-any.whl
pip3 install --no-deps Adafruit_PlatformDetect-3.19.1-py3-none-any.whl
pip3 install --no-deps Adafruit_PureIO-1.1.9-py3-none-any.whl
pip3 install --no-deps pyftdi-0.53.3-py3-none-any.whl
pip3 install --no-deps rpi_ws281x-4.3.1-cp39-cp39-linux_armv6l.whl
pip3 install --no-deps sysv_ipc-1.1.0-cp39-cp39-linux_armv6l.whl

We can test the library using the pcd8544_simpletest.py example. Beware to modify the source code, because the GPIOs used to communicate to the SPI device do not fit our model RASPBERRY_PI_B_REV2. Here are the values needed by our hardware wiring:

dc = digitalio.DigitalInOut(board.D23)  # data/command
cs = digitalio.DigitalInOut(board.D8)  # Chip select
reset = digitalio.DigitalInOut(board.D24)  # reset

Our LCD hardware works at best with display.bias = 5 and display.contrast = 57.

Installing the old Adafruit LCD library

As reported above, the old and discontinued library Adafruit Nokia LCD is less CPU intensive than the new Adafruit CircuitPython PCD8544. Fortunately enough, the old library does run with Python 3, and I was able to use it on my old Raspberry Pi model B over the operating system derived from Debian 11 Bullseye. So the average CPU usage is lower than 10%, instead than above 30%!

The library is not available from the PyPI repository, so it must be downloaded from the GitHub repository. The library depends upon Adafruit_GPIO, which in turn requires Adafruit_PureIO and spidev. The two Adafruit libraries are available from PyPI, for spidev we installed the Debian packaged version.

apt install python3-spidev
mkdir -p /usr/local/download/Adafruit-GPIO
cd /usr/local/download/Adafruit-GPIO
pip3 download Adafruit-GPIO
pip3 install Adafruit_PureIO-1.1.9-py3-none-any.whl
pip3 install Adafruit_GPIO-1.0.3-py3-none-any.whl

We download the library from its GitHub repository and install it using pip3:

cd /usr/local/download
git clone https://github.com/adafruit/Adafruit_Nokia_LCD.git
cd Adafruit_Nokia_LCD/
pip3 install .

The dependencies tree is the following:

  • Adafruit_Nokia_LCD
    • Adafruit_GPIO
      • Adafruit_PureIO
      • spidev

Installing the Telegram Bot Python library

FIXME: Into the Debian 11 Bullseye distribution there is the python3-python-telegram-bot package, which is a Telegram Bot Python library actively mantained, whereas the old Telepot library was abandoned. So we need to port our program to the new library and update these instruction.

We wish to save locally the Telegram client library telepot, so we can redo the installation in the future, even if the library will not be longer available in the current version. So into a directory e.g. called /usr/local/download/telepot/ we execute:

pip3 download telepot
...
Successfully downloaded telepot aiohttp urllib3 typing-extensions
    attrs async-timeout multidict chardet yarl idna

The library and its dependencies will be downloaded (not installed) in current directory. Now we install the dependencies from the Debian respository, if they are available (we prefer the Debian packaging over the Pip repository)

apt-get install python3-aiohttp python3-urllib3 python3-chardet python3-idna

Finally we install the telepot package from the local copy:

pip3 install telepot-12.7-py3-none-any.whl

Starting the protherm service at bootstrap

Raspberry Pi OS based on Debian 11 Bullseye uses systemd to start services on bootstrap. The protherm service requires the /dev/spidev0.0 device to be available on start, otherwise it starts with the LCD disabled. Unfortunately the timing at which the kernel enables the device cannot be predicted, so we have to take some precautions to let systemd wait for the spidev device before starting protherm.

First of all we create an udev rule; the rule will force systemd to create a “systemd device” when the kernel device becomes available. The rule is created into a file named /etc/udev/rules.d/protherm-spidev.rules:

# Inform systemd when the kernel device /dev/spidev0.0 becomes available.
# Systemd will create a systemd device named "dev-spidev0.0.device", which
# can be used to trigger the start of a service.
ACTION=="add", SUBSYSTEM=="spidev", KERNEL=="spidev0.0", TAG+="systemd"

We use three keywords that must matched (because of the == sign) during an udev event. If the event is of type device add, the device belongs to the spidev subsystem and the kernel device name is spidev0.0udev adds the string systemd to the TAG key associated to the device itself.

Systemd includes the systemd-udevd.service which will notice every device tagged witht the string systemd and dynamically creates a device unit. In this case the unit will be called dev-spidev0.0.device. You can ask systemd to list all the existing device units (because of the –all option, also the short name is listed):

systemctl list-units --all --type=device
...
  dev-spidev0.0.device   loaded active   plugged /dev/spidev0.0
...

If the systemd device does not exist, it probably means that the udev rule did not worked as expected. Thanks to this systemd device unit, we can write a protherm systemd unit that will start the service only after the device is instantiated by the kernel. The systemd unit will be /etc/systemd/system/protherm.service:

[Unit]
Description=Programmable Thermostat Service
After=syslog.target
# Wait for spidev0.0 systemd device to be available.
After=dev-spidev0.0.device
Requires=dev-spidev0.0.device

[Service]
Type=simple
WorkingDirectory=/tmp/
ExecStart=/usr/local/sbin/protherm -f

[Install]
WantedBy=multi-user.target

Source: Programmable Thermostat with the Raspberry Pi

Scroll to Top
Scroll to Top