Having all of the necessary pieces and some spare time during the holidays I resurrected a project that never reached completion to measure and plot temperature with a Raspberry Pi, in this case a Pi Zero but any Pi with a GPIO header will work, and two DS18B20 temperature sensors.
Assuming you have created an SD card with Raspberry Pi OS on it following the instructions here there are three additional steps needed to allow you to SSH into it on first boot. This can also be done through the desktop by connecting a keyboard, mouse and monitor but the Pi requires specific cables that you may not have e.g. mini-HDMI to HDMI.
/boot run touch ssh which creates an empty file with the name ssh and lets the OS know that ssh should be enabled on boot.userconf.txt with the single line below which creates a user named user (edit as appropriate) with password "raspberry" (change immediately after login with passwd). This is needed since Pi OS no longer creates the default user pi for security reasons and ssh will fail as there is simply no user defined yet.
user:$6$c70VpvPsVNCG0YR5$l5vWWLsLko9Kj65gcQ8qvMkuOoRkEagI90qi3F/Y7rm8eNYZHW8CY6BOIKwMH7a3YYzZYL90zf304cAHLFaZE0wpa_supplicant.conf and add the SSID and password for your WiFi network as below, you can add additional network{} blocks which I did because I was setting it up on one network and later moving it to another:
country=NL # Your 2-digit country code
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
network={
ssid="SSID"
psk="password"
key_mgmt=WPA-PSK
}
With the SD card inserted and the Pi Zero powered on it will connect to your WiFi and be ready to accept ssh connections from the user you defined, find the IP address from your router and connect.
After writing this I was trying to find out when the default pi user was removed and found this page explaining that the above steps can be done through the RPi Imager software as part of the SD card creation which may be slightly easier.
This is not absolutely necessary but gives a more detailed view of the GPIO header that we will be connecting to later. ssh to the Pi and start by installing this package:
sudo apt install python3-gpiozero
Then running the command pinout will generate the following output showing the physical layout of the Pi, some hardware configuration information and the pin assignments. Note the differences in the physical pin number and the GPIO number, these tripped me up briefly.

The DS18B20 temperature sensor is very easy to connect and several can be connected in parallel, I bought a pack of male-to-female jumper wires to connect to the GPIO header on the Pi. One of my sensors had yellow, red and black leads while another had yellow, red and blue, the jumper wires did not have all the matching colours, the only colour consistency in my setup is the red leads. A 4.7KΩ resistor is connected across the red and yellow leads of all sensors.


From a terminal on the Pi run sudo nano /boot/config.txt and add dtoverlay=w1-gpio to the end of the file, save and reboot. Then login and again from the terminal run:
sudo modprobe w1-gpio
sudo modprobe w1-therm
In the directory /sys/bus/w1/devices there will be one or more links to directories starting with the number 28, each of these links is a detected DS18B20 temperature sensor.
lrwxrwxrwx 1 root root 0 Dec 30 12:12 28-0000080923f1 -> ../../../devices/w1_bus_master1/28-0000080923f1
lrwxrwxrwx 1 root root 0 Dec 30 12:12 28-6885071e64ff -> ../../../devices/w1_bus_master1/28-6885071e64ff
lrwxrwxrwx 1 root root 0 Dec 30 12:26 w1_bus_master1 -> ../../../devices/w1_bus_master1
Going into any one of the directories and running cat w1_slave will show two lines of text similar to those below. The YES at the end of the first line indicates a valid measurement and the number at the end of the second line is the measured temperature in thousandths of a °C, the result below is 12.125°C since the sensor is in my garage.
c2 00 4b 46 7f ff 0e 10 04 : crc=04 YES
c2 00 4b 46 7f ff 0e 10 04 t=12125
The base code I used is taken from this page and extended to handle multiple sensors and to write the measurements to an SQLite database which I'll use later to plot the data. Most of the code is independent of the number of found sensors but the SQL query to insert data in the example below assumes two as making it dynamic requires more work than I want to put in. Save this as temperature.py. It will create the database if it doesn't exist or connect to it without overwriting if it does. It does the same for the table that contains the data. The script measures every 300 seconds.
# Python script to read from multiple DS18B20 temperature
# sensors and write the results to a SQL database
import os
import glob
import time
import datetime
from os.path import exists
import sqlite3
from sqlite3 import Error
os.system('modprobe w1-gpio')
os.system('modprobe w1-therm')
thermometer=[]
# When the sensor is connected to the GPIO pins it shows up
# as a device named w1_slave in a directory that begins with
# 28 located under the base_dir below
base_dir = '/sys/bus/w1/devices/'
devices = glob.glob(base_dir + '28*')
number_of_thermometers=len(devices)
for therm in range(number_of_thermometers):
thermometer.append(devices[therm] + '/w1_slave')
print("Found",number_of_thermometers,"devices")
# Database name and update frequency
update_frequency=300 # in seconds
db_name="SQLite-temp_multi_ds18b20.db"
# From https://www.sqlitetutorial.net/sqlite-python/insert/
# Create and/or connect to SQLite database
def create_connection(db_file):
conn = None
try:
conn = sqlite3.connect(db_file)
except Error as e:
print(e)
return conn
def read_temp_raw(device_file):
f = open(device_file, 'r')
lines = f.readlines()
f.close()
return lines
# Using the above function extract the temperature
# in millidegrees centigrade and convert to degrees
def read_temp(device_file):
lines = read_temp_raw(device_file)
# keep reading the file until we have a valid measurement
# this may be overkill but it's working
while lines[0].strip()[-3:] != 'YES':
time.sleep(0.2)
lines = read_temp_raw(device_file)
# look for the text with the value and convert it to celsius
equals_pos = lines[1].find('t=')
if equals_pos != -1:
temp_string = lines[1][equals_pos+2:]
temp_in_celsius = float(temp_string) / 1000.0
return temp_in_celsius
database = sqlite3.connect(db_name)
cursor = database.cursor()
print("Successfully Connected to SQLite")
try:
database.execute("SELECT * FROM temperature")
print("Table exists")
except database.OperationalError:
print("No table called temperature.")
if(database.OperationalError):
try:
table = """CREATE TABLE temperature (
Date_Time TEXT,
Temperature1 REAL,
Temperature2 REAL
); """
database.execute(table)
print("Created table")
except sqlite3.Error as e:
print(e, "occured")
try:
while True:
dt=datetime.datetime.now()
iso_time=dt.strftime('%Y-%m-%d %H:%M:%S')
temperature=[]
for sensor in range(number_of_thermometers):
temperature.append(read_temp(thermometer[sensor]))
# Open the database and insert time and temperature
database = create_connection(db_name)
cur = database.cursor()
sqlite_insert_query = """INSERT INTO temperature
(Date_Time, Temperature1, Temperature2)
VALUES (?, ?, ?) """
count = cur.execute(sqlite_insert_query, (iso_time, temperature[0], temperature[1]))
database.commit()
database.close()
print("Measurement", cur.lastrowid,": Temperature at", iso_time)
for sensor in range(number_of_thermometers):
print("Sensor",sensor,": ", temperature[sensor],"C", sep="")
time.sleep(update_frequency)
except sqlite3.Error as error:
print("Failed to insert data into sqlite table", error)
except KeyboardInterrupt:
print("Closing database")
In order to always have the measurements running if for example the Pi is rebooted I created a systemd service file called temperature.service (you can use any name) which should be saved in /etc/systemd/system/, make sure to edit the lines for WorkingDirectory and ExecStart to match the location of your script. These do not need to be the same place but I chose to have the SQLite database in the same directory as the python script.
[Unit]
Description=Measure ambient temperature with DS18B20 sensors
After=multi-user.target
[Service]
WorkingDirectory=/path/to/pythonscript
ExecStart=/usr/bin/python3 /path/to/pythonscript/temperature.py
Type=simple
Restart=always
RestartSec=10
StandardOutput=syslog
StandardError=syslog
[Install]
WantedBy=multi-user.target
This starts the service when the Pi starts.
sudo systemctl enable temperature
This can be used to start it manually in the background
sudo systemctl start temperature
I'm using Plotly to plot the measured data, I started with Matplotlib which was easier to get a static plot generated but ultimately less flexible. Assuming python3 is already installed which it should be on a default Pi OS install, run the following:
sudo pip3 install numpy plotly dash
If the below error happens when running the script below then an extensive error page appears including this link
libf77blas.so.3: cannot open shared object file: No such file or directory
which will suggest the solution below which worked, an alternative solution is proposed in addition but I didn't need/try that.
sudo apt-get install libatlas-base-dev
The code below is the result of many iterations and not from any one source, it assumes that the SQLite database has the same name as the one created by the measurement script and that it is in the same directory.
# Read date and temperature from an
# SQL database and plot it live with plotly
import pandas as pd
import plotly.express as px
from plotly.subplots import make_subplots
import plotly.graph_objects as go # or plotly.express as px
import dash
from dash import Dash, dcc, html, Input, Output
import sqlite3
from sqlite3 import Error
db_name="SQLite-temp_multi_ds18b20.db"
app = dash.Dash(__name__)
app.layout = html.Div(
[
dcc.Graph(id = 'live-graph', animate = False),
dcc.Interval(
id = 'graph-update',
interval = 10000,
n_intervals = 0
),
]
)
@app.callback(
Output('live-graph', 'figure'),
[ Input('graph-update', 'n_intervals') ]
)
def update_graph_scatter(n):
conn=sqlite3.connect(db_name)
df = pd.read_sql("select * from temperature", con=conn)
conn.close()
# next line reads from a .csv file, was replaced with the lines above
# df = pd.read_csv('temperature.csv')
# The column titles are defined when the database is created
# and the same ones have to be used here
fig = make_subplots(rows=2, cols=1)
# fig=go.Figure()
fig.add_trace(go.Histogram(x = df['Date_Time'], y = df['Temperature1'],
histfunc="avg", name="Daily Average",
xbins=dict(size="D1")),row=1,col=1)
fig.add_trace(go.Scatter(x = df['Date_Time'], y = df['Temperature1'],
name='Inside'),row=1,col=1)
fig.add_trace(go.Histogram(x = df['Date_Time'], y = df['Temperature2'],
histfunc="avg", name="Daily Average",
xbins=dict(size="D1")),row=2,col=1)
fig.add_trace(go.Scatter(x = df['Date_Time'], y = df['Temperature2'],
name='Outside'),row=2,col=1)
fig.update_layout(width=1200, height=800,bargap=0.1)
fig.update_layout(legend=dict(
orientation="h",
bgcolor="White",bordercolor="Black",
borderwidth=1,
yanchor="bottom", xanchor="center",
y=1.02,x=0.5),
font=dict(size=16))
fig.write_html("temperature.html")
return fig
if __name__ == '__main__':
app.run_server(host='0.0.0.0', port='5051')
Visiting http://pi-IP:5051 in a browser window displays an interactive plot that updates when new data is found. There is one plot for each sensor with all measured data points shown as a line and a histogram bar for each day that is the daily average. Despite the legend, both sensors are inside on the plot below, I need to find a way to position one of them outside.
Alternatively, the interactive Plotly plot can be exported to a fully self-contained html file using:
fig.write_html("temperature.html")`
and included in this post via
<iframe width="750" height="600" name="iframe" src="temperature.html"></iframe>