Pi Weather Display Project

This project is so cobbled together I’m almost proud of it, I don’t expect anyone to be able to recreate this exactly but if there are parts of it that help, here you go.

My dog prefers a walk early in the morning as the sun is dawning, so I’d like info on when dawn is along with the current weather forecast, plus very localised info on what the temperature is now and importantly, if it’s raining out.

The go to choice to present this info would be a Raspberry Pi with a small display to allow a quick glance at in the morning.

The info to display on the Pi screen can be sources from a couple of sources;

  • An API call from a weather service (weatherbit.io) for more general sunrise, sunset and current weather.
  • A SDR radio picking up a nearby weather station for more localised temperature and rainfall values.

This is how it comes together…

Concept

The plan is to keep this as simple as possible.

General weather data is to be provided from a weather service via an API call and can parse the required data for display on the screen. weatherbit.io is able to provide this for free up to 50 calls per day.

For local data from a nearby weather station, a SDR is needed to pick up the signals for decoding and parsing. In my case I already have an SDR connected to another Pi located in the attic for amateur radio, so can tap into this to pick up local stations.

To display this for quick reference, a local webpage that runs on Chromium in kiosk seems the easiest way to get the info on the screen. Rather than creating a dynamic webpage to pull the data, have a script create the html that includes a simple refresh to deal with the page changing when the data updates.

Hardware

In its most basic form, the main hardware in use:

However, the Pi 4 is a bit overkill, so I would like it to do double duty as a data store, and house it neatly in a custom case. Additional components would include:

Case

For the most compact packaging a custom 3D printed case would be created.

From previous attempts its designed to be a self-contained box, the design of this incorporated the following features:

  • Screwless / glueless design.
  • Incorporate a RJ45 socket to allow all ports to be located at the back.
  • Main io of Pi facing the rear for easy power connection.
  • Uses ribbon cable to secure / cushion HDD in place and to allow for tolerances.

Download STL files

Weatherbit API

After some trial and error, weatherbit.io seemed the best choice for easy access to weather data for the minimal of personal data, it does require signup for access to the API but is not too intrusive.

The free version for personal use allows up to 50 calls per day, allowing for a weather update every 30 minutes.

The output is quite detailed, and can be used as the only source for a weather display:

{"alerts":[],"count":1,"data":[{"app_temp":17.6,"aqi":65,"city_name":"NA","clouds":0,"country_code":"GB","datetime":"2024-08-31:12","dewpt":12.4,"dhi":110,"dni":864,"elev_angle":45.91,"ghi":723,"gust":12.3,"h_angle":0,"lat":NA,"lon":NA,"ob_time":"2024-08-31 12:48","pod":"d","precip":0,"pres":1008,"rh":72,"slp":1022,"snow":0,"solar_rad":723,"sources":["analysis","radar","satellite"],"state_code":"NA","station":"E0000","sunrise":"05:25","sunset":"19:00","temp":17.6,"timezone":"Europe/London","ts":1725108496,"uv":6,"vis":16,"weather":{"description":"Clear sky","code":800,"icon":"c01d"},"wind_cdir":"NE","wind_cdir_full":"northeast","wind_dir":55,"wind_spd":6.5}]}

Radio

I already have a separate Raspiberry Pi with an SDR attached. This uses rtl_tcp to allow connections to the SDR from software on other machines via the network. This works great to allow the SDR / antenna as high up as possible in my loft, and control it from my main PC at ground level.

Collection of data from a weather station comes from RTL 433, a fantastic tool that decodes data from a variety of devices on the 433mhz.

In order to integrate with my current setup, RTL 433 will connect to the SDR via rtl_tcp occasionally to collect latest data as only one device can use the SDR at a time. This allows my to “hijack” the SDR should I want to scan the radio.

My original scope was to use my personal weather station that only recorded temperature and humidity, but since using RTL 433 I’ve found a more advanced weather station nearby that can record rainfall and wind data, great for my project.

This station transmits its data every 60 seconds. So, to get this data I’ll run a scan via the SDR for 90 seconds every 10 minutes. This allows at least a recent transmit of the station but giving enough down time for me to step in and hijack the SDR when needed.

A SDR stream is 40Mbps, to save this going across the network every 10 minutes the script to collect data will be ran locally. The RTL 433 program can output to a json, and SNMP will be used to serve weather data to the display Pi.

Script

Bringing everything together requires some timing; SDR every ten minutes, API every half hour. Making this as easy as possible, the script is split into different sections and run at different times via cron jobs.

Collect SDR Data

This script is run on the SDR Pi and collects the radio data for 90 seconds, outputting data to a json file. This is set to run every 10 minutes:

Cron:

*/10 * * * * /home/pi/433_sensors/433_run.sh

Script:

#!/bin/bash
# Collect data for 90 seconds

timeout 90 rtl_433 -d rtl_tcp:10.0.1.242:1234 -F json:/home/pi/433_sensors/433_json.txt

The latest data is then offered to the rest of the network via SNMP…

Addition to snmpd.conf:

# Temperature - 433mhz radio
extend-sh temp_radio /etc/snmp/snmp-433mhz-temps.sh

snmp-433mhz-temps.sh

#!/bin/bash

######## Output order #########
#
# my sensor – temperature
# my sensor – humidity
# local ws – temperature
# local ws – humidity
# local ws - wind direction (degrees)
# local ws - wind average (mph)
# local ws - wind max (mph)
# local ws - rain (mm)
#
###############################

## Bresser-3CH - My temperature sensor ##

# JSON data file
JSON_FILE="/home/pi/433_sensors/433_json.txt"

# Grab latest entry for model Bresser-3CH
latest_entry=$(jq -r 'select(.model == "Bresser-3CH") | [.time, .temperature_F, .humidity] | @tsv' "$JSON_FILE" | sort -r | head -n 1)

# Check if the latest_entry is empty
if [ -z "$latest_entry" ]; then
  echo "No data found for model Bresser-3CH"
  exit 1
fi

# Extract time, temp and humidity from Bresser-3CH
latest_time=$(echo "$latest_entry" | awk -F'\t' '{print $1}')
temperature_F=$(echo "$latest_entry" | awk -F'\t' '{print $2}')
humidity=$(echo "$latest_entry" | awk -F'\t' '{print $3}')

# Convert temperature from Fahrenheit to Celsius and format to 1 decimal place
temperature_C=$(echo "scale=1; ($temperature_F - 32) * 5 / 9" | bc)

# Output results for SNMP collection
echo "$temperature_C"
echo "$humidity"

## WHx080 local weather station ##

# Grab latest entry for model Fineoffset-WHx080
latest_fineoffset_entry=$(jq -r 'select(.model == "Fineoffset-WHx080") | [.time, .temperature_C, .humidity, .wind_dir_deg, .wind_avg_km_h, .wind_max_km_h, .rain_mm] | @tsv' "$JSON_FILE" | sort -r | head -n 1)

# Check if the latest_fineoffset_entry is empty
if [ -z "$latest_fineoffset_entry" ]; then
  echo "No data found for model Fineoffset-WHx080"
  exit 1
fi

# Extract data from Fineoffset-WHx080
temperature_C_fineoffset=$(echo "$latest_fineoffset_entry" | awk -F'\t' '{print $2}')
humidity_fineoffset=$(echo "$latest_fineoffset_entry" | awk -F'\t' '{print $3}')
wind_dir_deg=$(echo "$latest_fineoffset_entry" | awk -F'\t' '{print $4}')
wind_avg_km_h=$(echo "$latest_fineoffset_entry" | awk -F'\t' '{print $5}')
wind_max_km_h=$(echo "$latest_fineoffset_entry" | awk -F'\t' '{print $6}')
rain_mm=$(echo "$latest_fineoffset_entry" | awk -F'\t' '{print $7}')

# Convert wind speeds from km/h to mph
wind_avg_mph=$(echo "scale=1; $wind_avg_km_h * 0.621371" | bc)
wind_max_mph=$(echo "scale=1; $wind_max_km_h * 0.621371" | bc)

# Output results for SNMP collection
echo "$temperature_C_fineoffset"
echo "$humidity_fineoffset"
echo "$wind_dir_deg"
echo "$wind_avg_mph"
echo "$wind_max_mph"
echo "$rain_mm"

Collect SDR data on Weather Display

This will collect the SDR data presented over the network via SNMP. This also runs every 10 minutes but is offset by a couple of minutes to ensure the most up to date values are collected.

Cron:

2,12,22,32,42,52 * * * * /home/pi/scripts/weather_data/generate_radio.sh

Script:

#!/bin/bash

# Collect weather data from SNMP and output to file.

# File paths
DATA_SNMP_TEMP="/home/pi/scripts/weather_data/data_snmp_temp.txt"
DATA_SNMP_HUMID="/home/pi/scripts/weather_data/data_snmp_humid.txt"
DATA_SNMP_RAIN="/home/pi/scripts/weather_data/data_snmp_rain.txt"
DATA_SNMP_RAIN_PREV="/home/pi/scripts/weather_data/data_snmp_rain_prev.txt"
DATA_SNMP_RAIN_FLAG="/home/pi/scripts/weather_data/data_snmp_rain_flag.txt"
DATA_SNMP_RAIN_COL="/home/pi/scripts/weather_data/data_snmp_rain_col.txt"

# Fetch temperature
SNMP_COMMAND='snmpget -v 2c 10.0.1.242 -c public NET-SNMP-EXTEND-MIB::nsExtendOutLine."temp_radio".1'

# Execute command
SNMP_RESULT=$($SNMP_COMMAND 2>&1)

# Check command did not return a timeout
if echo "$SNMP_RESULT" | grep -qi "timeout"; then
    echo "SNMP temperature request timed out, skipping."
else
    # If no timeout, save the output to file
    echo "$SNMP_RESULT" | awk '{print $NF}' > "$DATA_SNMP_TEMP"
    echo "SNMP temperature data saved"
fi

# Fetch humidity
SNMP_COMMAND='snmpget -v 2c 10.0.1.242 -c public NET-SNMP-EXTEND-MIB::nsExtendOutLine."temp_radio".2'

# Execute command
SNMP_RESULT=$($SNMP_COMMAND 2>&1)
# Check command did not return a timeout
if echo "$SNMP_RESULT" | grep -qi "timeout"; then
    echo "SNMP humidity request timed out, skipping."
else
    # If no timeout, save the output to file
    echo "$SNMP_RESULT" | awk '{print $NF}' > "$DATA_SNMP_HUMID"
    echo "SNMP humidity data saved"
fi

# Prepare files for rain flag
cp "$DATA_SNMP_RAIN" "$DATA_SNMP_RAIN_PREV"

# Fetch rain
SNMP_COMMAND='snmpget -v 2c 10.0.1.242 -c public NET-SNMP-EXTEND-MIB::nsExtendOutLine."temp_radio".8'

# Execute command
SNMP_RESULT=$($SNMP_COMMAND 2>&1)

# Check command did not return a timeout
if echo "$SNMP_RESULT" | grep -qi "timeout"; then
    echo "SNMP rain request timed out, skipping."
else
    # If no timeout, save the output to file
    echo "$SNMP_RESULT" | awk '{print $NF}' > "$DATA_SNMP_RAIN"
    echo "SNMP rain data saved"
fi

# Check if rain counter increased since last result
RAIN_PREV=$(cat "$DATA_SNMP_RAIN_PREV")
RAIN_NOW=$(cat "$DATA_SNMP_RAIN")

if (( $(echo "$RAIN_NOW > $RAIN_PREV" | bc -l) )); then
    # Output for display
    RAIN_STATUS="Yes"
    # Set text colour for display
    RAIN_COLOUR="#49b2fc"
else
    RAIN_STATUS="No"
    RAIN_COLOUR="#ffffff"
fi

echo "Raining: $RAIN_STATUS"
echo "$RAIN_STATUS" > "$DATA_SNMP_RAIN_FLAG"
echo "$RAIN_COLOUR" > "$DATA_SNMP_RAIN_COL"

Collect API Data

This pulls the weather from the API and extracts the bits we need to a file. While the API allows 50 calls a day, I’d found it returning a 403 in the early afternoon when call was set to 30 minutes. Therefore, its set to call every hour.

Cron:

0 * * * * /home/pi/scripts/weather_data/generate_api.sh

Script:

#!/bin/bash
# Pulls latest data from weatherbit api

# File paths
DATA_WEATHER_API="/home/pi/scripts/weather_data/data_api.json"
DATA_API_SUNRISE="/home/pi/scripts/weather_data/data_api_sunrise.txt"
DATA_API_SUNSET="/home/pi/scripts/weather_data/data_api_sunset.txt"
DATA_API_DESC="/home/pi/scripts/weather_data/data_api_desc.txt"
DATA_API_ICON="/home/pi/scripts/weather_data/data_api_icon.txt"

# Fetch Data
WEATHER_DATA=$(curl -X GET --header 'Accept: application/json' 'https://api.weatherbit.io/v2.0/current?lat=51.605817&lon=-3.091024&include=alerts&key=00000000000000000000000000000000')

# Check fetch was successful
if [ $? -ne 0 ]; then
    echo "API Call failed" >&2
    exit 1
else
    # If successful, write this data to file
    echo "$WEATHER_DATA" > "$DATA_WEATHER_API"
    echo "Weather data saved"
fi

# Extract data we need
SUNRISE=$(jq -r '.data[0].sunrise' < "$DATA_WEATHER_API")
SUNSET=$(jq -r '.data[0].sunset' < "$DATA_WEATHER_API")
WEATHER_DESC=$(jq -r '.data[0].weather.description' < "$DATA_WEATHER_API")
WEATHER_ICON=$(jq -r '.data[0].weather.icon' < "$DATA_WEATHER_API")

# API returns UTC time, need to adjust for BST
# Function to check current date is within the BST period
is_bst() {
    # Use system time to check if we’re in BST
    [[ $(date +%Z) == "BST" ]]
}

# Function to add 1 hour to the time
adjust_for_bst() {
    local time="$1"
    date -d "$time today + 1 hour" +%H:%M
}

# Check if it's currently BST
if is_bst; then
    # Add an hour to sunrise and sunset
    SUNRISE=$(adjust_for_bst "$SUNRISE")
    SUNSET=$(adjust_for_bst "$SUNSET")
fi

# Save extracted API data to separate files
echo "$SUNRISE" > "$DATA_API_SUNRISE"
echo "$SUNSET" > "$DATA_API_SUNSET"
echo "$WEATHER_DESC" > "$DATA_API_DESC"
echo "$WEATHER_ICON" > "$DATA_API_ICON"

Generate HTML

This brings together all collected values and inserts them into a html temple, the html file is replaced each run so also has a refresh mechanism to ensure it updates on the screen.

Like the SDR generation, this is ran every 10 minutes and offset to ensure most recently data is available to display.

Cron:

3,13,23,33,43,53 * * * * /home/pi/scripts/weather_data/generate_html.sh

Script:

#!/bin/bash
# Combines data from radio and api and creates html file for display

# File paths
DATA_SNMP_TEMP="/home/pi/scripts/weather_data/data_snmp_temp.txt"
DATA_SNMP_HUMID="/home/pi/scripts/weather_data/data_snmp_humid.txt"
#DATA_SNMP_RAIN="/home/pi/scripts/weather_data/data_snmp_rain.txt"
#DATA_SNMP_RAIN_PREV="/home/pi/scripts/weather_data/data_snmp_rain_prev.txt"
DATA_SNMP_RAIN_FLAG="/home/pi/scripts/weather_data/data_snmp_rain_flag.txt"
DATA_SNMP_RAIN_COL="/home/pi/scripts/weather_data/data_snmp_rain_col.txt"
DATA_WEATHER_API="/home/pi/scripts/weather_data/data_api.json"
DATA_API_SUNRISE="/home/pi/scripts/weather_data/data_api_sunrise.txt"
DATA_API_SUNSET="/home/pi/scripts/weather_data/data_api_sunset.txt"
DATA_API_DESC="/home/pi/scripts/weather_data/data_api_desc.txt"
DATA_API_ICON="/home/pi/scripts/weather_data/data_api_icon.txt"
DATA_HTML="/var/www/html/weather.html"

# Load latest data
TEMP=$(cat "$DATA_SNMP_TEMP")
HUMID=$(cat "$DATA_SNMP_HUMID")
RAIN=$(cat "$DATA_SNMP_RAIN_FLAG")
RAIN_COL=$(cat "$DATA_SNMP_RAIN_COL")
SUNRISE=$(cat "$DATA_API_SUNRISE")
SUNSET=$(cat "$DATA_API_SUNSET")
DESC=$(cat "$DATA_API_DESC")
ICON=$(cat "$DATA_API_ICON")

# Generate HTML
echo "Generating HTML content..."
cat << EOF > "$DATA_HTML"
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="refresh" content="60"> <!-- Refresh every 60 seconds -->
    <title>Weather Display</title>
    <style>
        body { font-family: Arial, sans-serif; font-weight: bold; color: white;}
        .container { width: 100%; margin: 0 auto; text-align: center; }
    </style>
</head>
<body style="background-color:black;">
    <div class="container">
       <table style="width:100%"><thead>
         <tr>
           <th colspan="2" rowspan="2"><img src="images/icons/$ICON.png" width="120" height="120"></th>
           <th colspan="2" style="text-align: left; font-size: 36px;">$DESC</th>
         </tr>
         <tr>
           <th style="text-align: right;"><img src="images/temperature.png" width="64" height="64"></th>
           <th style="font-size: 56px;">${TEMP}C</th>
         </tr></thead>
       <tbody>
         <tr>
           <td><img src="images/sunrise.png" width="64" height="64"></td>
           <td style="font-size: 48px;">$SUNRISE</td>
           <td style="text-align: right;"><img src="images/humidity.png" width="64" height="64"></td>
       <td style="font-size: 56px;">$HUMID%</td>
         </tr>
         <tr>
           <td><img src="images/sunset.png" width="64" height="64"></td>
           <td style="font-size: 48px;">$SUNSET</td>
           <td style="text-align: right;"><img src="images/rain.png" width="64" height="64"></td>
       <td style="font-size: 56px; color:${RAIN_COL};">$RAIN</td>
         </tr>
       </tbody>
       </table>
    </div>
</body>
</html>
EOF

Screen Display

Finally, time to get the data on the screen. For ease of use the webpage should start with the system so there is no configuring or manual execution after each reboot.

A service would be best, create the file:

/etc/systemd/system/pidisplay.service

Add this to the file:

[Unit]
Description=Launches Chromium in kiosk mode for my display.

[Service]
ExecStart=/home/pi/scripts/display_launch.sh
WorkingDirectory=/home/pi/scripts/
User=pi
Group=pi
Restart=always
Environment=DISPLAY=:0
Environment=XAUTHORITY=/home/pi/.Xauthority

[Install]
WantedBy=multi-user.target

Reload services and enable / start the service:

sudo systemctl daemon-reload
sudo systemctl enable --now pidisplay.service

Conclusion

All this in place and you have yourself a weather display. As mentioned at the top it is a bit of a cobbled together setup and way overkill. But the result is a quick reference display for when its still dark out.

Update

After a few months of operation it is working well, and with a relief that the daylight savings adjustment to the sunrise / sunset times worked without issue.

One issue that cropped up after a month’s deployment saw the radio data not being updated anymore.

The cause was the rtl433 output file becoming too large, as the JQ query scans the whole file to find the latest record, the SNMP get request was timing out before JQ had a change to find the most recent entry.

This is something a cron job to split and archive the output can solve.

Cron:

Run the scricpt every Monday at 00:05.

5 0 * * 1 /home/pi/433_sensors/433_json_archive.sh

Script:

#!/bin/bash

# Script to cleanup database, leave newest 1M at original filename.
# Note: If lines are not split cleanly, it'll cause snmp to fail.

# File paths
ORIGINAL_LOG="/home/pi/433_sensors/433_json.txt"
RECENT_LOG="/home/pi/433_sensors/temp_recent.txt"
LOG_ARCHIVE_DIR="/home/pi/433_sensors/log_archive"

# Timestamp in format YYYYMMDD-HHMMSS)
TIMESTAMP=$(date +"%Y%m%d-%H%M%S")

# Give a filename for old records (will gzip later)
OLDER_LOG="/home/pi/433_sensors/433_json_$TIMESTAMP.log"

# Split last 1000 lines to new file
tail -n 1000 $ORIGINAL_LOG > $RECENT_LOG

# Split previous lines upto 1000 to another file
head -n -1000 $ORIGINAL_LOG > $OLDER_LOG

# Put last 1000 lines back to original filename
mv $RECENT_LOG $ORIGINAL_LOG

# Compress and move old log to archive dir.
gzip $OLDER_LOG
mv "${OLDER_LOG}.gz" $LOG_ARCHIVE_DIR

Why am I keeping the old logs? Well I may one day like to expand what I can monitor via this method, namely tire pressures on my car. However I need to identify and note the frequency of thesse transmissions before I can add to monitoring. This may be a future project.