{"id":975,"date":"2024-12-03T17:50:00","date_gmt":"2024-12-03T17:50:00","guid":{"rendered":"https:\/\/james-batchelor.com\/?p=975"},"modified":"2025-02-16T16:27:48","modified_gmt":"2025-02-16T16:27:48","slug":"pi-weather-display-project","status":"publish","type":"post","link":"https:\/\/james-batchelor.com\/index.php\/2024\/12\/03\/pi-weather-display-project\/","title":{"rendered":"Pi Weather Display Project"},"content":{"rendered":"\n<p>This project is so cobbled together I\u2019m almost proud of it, I don\u2019t expect anyone to be able to recreate this exactly but if there are parts of it that help, here you go.<\/p>\n\n\n\n<p>My dog prefers a walk early in the morning as the sun is dawning, so I\u2019d 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\u2019s raining out.<\/p>\n\n\n\n<p>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.<\/p>\n\n\n\n<p>The info to display on the Pi screen can be sources from a couple of sources;<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>An API call from a weather service (weatherbit.io) for more general sunrise, sunset and current weather.<\/li>\n\n\n\n<li>A SDR radio picking up a nearby weather station for more localised temperature and rainfall values.<\/li>\n<\/ul>\n\n\n\n<figure class=\"wp-block-image size-full\"><a href=\"https:\/\/james-batchelor.com\/wp-content\/uploads\/2024\/12\/20241205_155902-2.jpg\"><img loading=\"lazy\" decoding=\"async\" width=\"1920\" height=\"1059\" src=\"https:\/\/james-batchelor.com\/wp-content\/uploads\/2024\/12\/20241205_155902-2.jpg\" alt=\"\" class=\"wp-image-989\"\/><\/a><\/figure>\n\n\n\n<p>This is how it comes together\u2026<\/p>\n\n\n\n<!--more-->\n\n\n\n<h2 class=\"wp-block-heading\">Concept<\/h2>\n\n\n\n<p>The plan is to keep this as simple as possible.<\/p>\n\n\n\n<p>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.<\/p>\n\n\n\n<p>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.<\/p>\n\n\n\n<p>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.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Hardware<\/h2>\n\n\n\n<p>In its most basic form, the main hardware in use:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Raspberry Pi 4 2GB (Although a Pi 2B or higher will likely suffice)<\/li>\n\n\n\n<li><a href=\"https:\/\/www.amazon.co.uk\/dp\/B01MRQTMTD?ref=ppx_yo2ov_dt_b_fed_asin_title\" target=\"_blank\" rel=\"noreferrer noopener\">ELEGOO 3.5 Inch TFT LCD 480&#215;320 Screen<\/a><\/li>\n<\/ul>\n\n\n\n<p>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:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/www.amazon.co.uk\/Toshiba-HDTB440EK3CA-2-5-Inch-Portable-External\/dp\/B07KPM58FX\" target=\"_blank\" rel=\"noreferrer noopener\">Toshiba 4TB USB HDD<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/www.amazon.co.uk\/Elegoo-120pcs-Multicolored-Breadboard-arduino-colorful\/dp\/B01EV70C78\" target=\"_blank\" rel=\"noreferrer noopener\">40 pin ribbon cable to locate the screen away from the Pi board<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/www.amazon.co.uk\/LEENUE-Ethernet-Faceplate-Network-Keystone-White\/dp\/B091D4PMCJ\" target=\"_blank\" rel=\"noreferrer noopener\">RJ45 socket for neat connection of Ethernet cable to the case<\/a><\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Case<\/h2>\n\n\n\n<p>For the most compact packaging a custom 3D printed case would be created.<\/p>\n\n\n\n<p>From previous attempts its designed to be a self-contained box, the design of this incorporated the following features:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Screwless \/ glueless design.<\/li>\n\n\n\n<li>Incorporate a RJ45 socket to allow all ports to be located at the back.<\/li>\n\n\n\n<li>Main io of Pi facing the rear for easy power connection.<\/li>\n\n\n\n<li>Uses ribbon cable to secure \/ cushion HDD in place and to allow for tolerances.<\/li>\n<\/ul>\n\n\n\n<figure class=\"wp-block-image size-full\"><a href=\"https:\/\/james-batchelor.com\/wp-content\/uploads\/2024\/10\/wd-stl-01.jpg\"><img loading=\"lazy\" decoding=\"async\" width=\"732\" height=\"642\" src=\"https:\/\/james-batchelor.com\/wp-content\/uploads\/2024\/10\/wd-stl-01.jpg\" alt=\"\" class=\"wp-image-980\"\/><\/a><\/figure>\n\n\n\n<figure class=\"wp-block-image size-full\"><a href=\"https:\/\/james-batchelor.com\/wp-content\/uploads\/2024\/10\/wd-stl-02.jpg\"><img loading=\"lazy\" decoding=\"async\" width=\"703\" height=\"539\" src=\"https:\/\/james-batchelor.com\/wp-content\/uploads\/2024\/10\/wd-stl-02.jpg\" alt=\"\" class=\"wp-image-981\"\/><\/a><\/figure>\n\n\n\n<p><\/p>\n\n\n\n<p class=\"has-text-align-center\"><a rel=\"noreferrer noopener\" href=\"https:\/\/james-batchelor.com\/wp-content\/uploads\/2024\/10\/Pi_Weather_Display.zip\" target=\"_blank\">Download STL files<\/a><\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><a href=\"https:\/\/james-batchelor.com\/wp-content\/uploads\/2025\/02\/20250111_103918.jpg\"><img loading=\"lazy\" decoding=\"async\" width=\"3837\" height=\"720\" src=\"https:\/\/james-batchelor.com\/wp-content\/uploads\/2025\/02\/20250111_103918.jpg\" alt=\"\" class=\"wp-image-998\"\/><\/a><\/figure><\/div>\n\n\n<h2 class=\"wp-block-heading\">Weatherbit API<\/h2>\n\n\n\n<p>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.<\/p>\n\n\n\n<p>The free version for personal use allows up to 50 calls per day, allowing for a weather update every 30 minutes.<\/p>\n\n\n\n<p>The output is quite detailed, and can be used as the only source for a weather display:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\"alerts\":&#91;],\"count\":1,\"data\":&#91;{\"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\":&#91;\"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}]}<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Radio<\/h2>\n\n\n\n<p>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.<\/p>\n\n\n\n<p>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.<\/p>\n\n\n\n<p>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 \u201chijack\u201d the SDR should I want to scan the radio.<\/p>\n\n\n\n<p>My original scope was to use my personal weather station that only recorded temperature and humidity, but since using RTL 433 I\u2019ve found a more advanced weather station nearby that can record rainfall and wind data, great for my project.<\/p>\n\n\n\n<p>This station transmits its data every 60 seconds. So, to get this data I\u2019ll 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.<\/p>\n\n\n\n<p>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.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Script<\/h2>\n\n\n\n<p>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.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Collect SDR Data<\/h3>\n\n\n\n<p>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:<\/p>\n\n\n\n<p>Cron:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>*\/10 * * * * \/home\/pi\/433_sensors\/433_run.sh<\/code><\/pre>\n\n\n\n<p>Script:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>#!\/bin\/bash<br># Collect data for 90 seconds<br><br>timeout 90 rtl_433 -d rtl_tcp:10.0.1.242:1234 -F json:\/home\/pi\/433_sensors\/433_json.txt<br><br>The latest data is then offered to the rest of the network via SNMP\u2026<\/code><\/pre>\n\n\n\n<p>Addition to snmpd.conf:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Temperature - 433mhz radio<br>extend-sh temp_radio \/etc\/snmp\/snmp-433mhz-temps.sh<\/code><\/pre>\n\n\n\n<p>snmp-433mhz-temps.sh<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>#!\/bin\/bash<br><br>######## Output order #########<br>#<br># my sensor \u2013 temperature<br># my sensor \u2013 humidity<br># local ws \u2013 temperature<br># local ws \u2013 humidity<br># local ws - wind direction (degrees)<br># local ws - wind average (mph)<br># local ws - wind max (mph)<br># local ws - rain (mm)<br>#<br>###############################<br><br>## Bresser-3CH - My temperature sensor ##<br><br># JSON data file<br>JSON_FILE=\"\/home\/pi\/433_sensors\/433_json.txt\"<br><br># Grab latest entry for model Bresser-3CH<br>latest_entry=$(jq -r 'select(.model == \"Bresser-3CH\") | &#91;.time, .temperature_F, .humidity] | @tsv' \"$JSON_FILE\" | sort -r | head -n 1)<br><br># Check if the latest_entry is empty<br>if &#91; -z \"$latest_entry\" ]; then<br>&nbsp; echo \"No data found for model Bresser-3CH\"<br>&nbsp; exit 1<br>fi<br><br># Extract time, temp and humidity from Bresser-3CH<br>latest_time=$(echo \"$latest_entry\" | awk -F'\\t' '{print $1}')<br>temperature_F=$(echo \"$latest_entry\" | awk -F'\\t' '{print $2}')<br>humidity=$(echo \"$latest_entry\" | awk -F'\\t' '{print $3}')<br><br># Convert temperature from Fahrenheit to Celsius and format to 1 decimal place<br>temperature_C=$(echo \"scale=1; ($temperature_F - 32) * 5 \/ 9\" | bc)<br><br># Output results for SNMP collection<br>echo \"$temperature_C\"<br>echo \"$humidity\"<br><br>## WHx080 local weather station ##<br><br># Grab latest entry for model Fineoffset-WHx080<br>latest_fineoffset_entry=$(jq -r 'select(.model == \"Fineoffset-WHx080\") | &#91;.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)<br><br># Check if the latest_fineoffset_entry is empty<br>if &#91; -z \"$latest_fineoffset_entry\" ]; then<br>&nbsp; echo \"No data found for model Fineoffset-WHx080\"<br>&nbsp; exit 1<br>fi<br><br># Extract data from Fineoffset-WHx080<br>temperature_C_fineoffset=$(echo \"$latest_fineoffset_entry\" | awk -F'\\t' '{print $2}')<br>humidity_fineoffset=$(echo \"$latest_fineoffset_entry\" | awk -F'\\t' '{print $3}')<br>wind_dir_deg=$(echo \"$latest_fineoffset_entry\" | awk -F'\\t' '{print $4}')<br>wind_avg_km_h=$(echo \"$latest_fineoffset_entry\" | awk -F'\\t' '{print $5}')<br>wind_max_km_h=$(echo \"$latest_fineoffset_entry\" | awk -F'\\t' '{print $6}')<br>rain_mm=$(echo \"$latest_fineoffset_entry\" | awk -F'\\t' '{print $7}')<br><br># Convert wind speeds from km\/h to mph<br>wind_avg_mph=$(echo \"scale=1; $wind_avg_km_h * 0.621371\" | bc)<br>wind_max_mph=$(echo \"scale=1; $wind_max_km_h * 0.621371\" | bc)<br><br># Output results for SNMP collection<br>echo \"$temperature_C_fineoffset\"<br>echo \"$humidity_fineoffset\"<br>echo \"$wind_dir_deg\"<br>echo \"$wind_avg_mph\"<br>echo \"$wind_max_mph\"<br>echo \"$rain_mm\"<br><\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Collect SDR data on Weather Display<\/h3>\n\n\n\n<p>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.<\/p>\n\n\n\n<p>Cron:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>2,12,22,32,42,52 * * * * \/home\/pi\/scripts\/weather_data\/generate_radio.sh<\/code><\/pre>\n\n\n\n<p>Script:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>#!\/bin\/bash<br><br># Collect weather data from SNMP and output to file.<br><br># File paths<br>DATA_SNMP_TEMP=\"\/home\/pi\/scripts\/weather_data\/data_snmp_temp.txt\"<br>DATA_SNMP_HUMID=\"\/home\/pi\/scripts\/weather_data\/data_snmp_humid.txt\"<br>DATA_SNMP_RAIN=\"\/home\/pi\/scripts\/weather_data\/data_snmp_rain.txt\"<br>DATA_SNMP_RAIN_PREV=\"\/home\/pi\/scripts\/weather_data\/data_snmp_rain_prev.txt\"<br>DATA_SNMP_RAIN_FLAG=\"\/home\/pi\/scripts\/weather_data\/data_snmp_rain_flag.txt\"<br>DATA_SNMP_RAIN_COL=\"\/home\/pi\/scripts\/weather_data\/data_snmp_rain_col.txt\"<br><br># Fetch temperature<br>SNMP_COMMAND='snmpget -v 2c 10.0.1.242 -c public NET-SNMP-EXTEND-MIB::nsExtendOutLine.\"temp_radio\".1'<br><br># Execute command<br>SNMP_RESULT=$($SNMP_COMMAND 2&gt;&amp;1)<br><br># Check command did not return a timeout<br>if echo \"$SNMP_RESULT\" | grep -qi \"timeout\"; then<br>&nbsp;&nbsp;&nbsp; echo \"SNMP temperature request timed out, skipping.\"<br>else<br>&nbsp;&nbsp;&nbsp; # If no timeout, save the output to file<br>&nbsp;&nbsp;&nbsp; echo \"$SNMP_RESULT\" | awk '{print $NF}' &gt; \"$DATA_SNMP_TEMP\"<br>&nbsp;&nbsp;&nbsp; echo \"SNMP temperature data saved\"<br>fi<br><br># Fetch humidity<br>SNMP_COMMAND='snmpget -v 2c 10.0.1.242 -c public NET-SNMP-EXTEND-MIB::nsExtendOutLine.\"temp_radio\".2'<br><br># Execute command<br>SNMP_RESULT=$($SNMP_COMMAND 2&gt;&amp;1)<br># Check command did not return a timeout<br>if echo \"$SNMP_RESULT\" | grep -qi \"timeout\"; then<br>&nbsp;&nbsp;&nbsp; echo \"SNMP humidity request timed out, skipping.\"<br>else<br>&nbsp;&nbsp;&nbsp; # If no timeout, save the output to file<br>&nbsp;&nbsp;&nbsp; echo \"$SNMP_RESULT\" | awk '{print $NF}' &gt; \"$DATA_SNMP_HUMID\"<br>&nbsp;&nbsp;&nbsp; echo \"SNMP humidity data saved\"<br>fi<br><br># Prepare files for rain flag<br>cp \"$DATA_SNMP_RAIN\" \"$DATA_SNMP_RAIN_PREV\"<br><br># Fetch rain<br>SNMP_COMMAND='snmpget -v 2c 10.0.1.242 -c public NET-SNMP-EXTEND-MIB::nsExtendOutLine.\"temp_radio\".8'<br><br># Execute command<br>SNMP_RESULT=$($SNMP_COMMAND 2&gt;&amp;1)<br><br># Check command did not return a timeout<br>if echo \"$SNMP_RESULT\" | grep -qi \"timeout\"; then<br>&nbsp;&nbsp;&nbsp; echo \"SNMP rain request timed out, skipping.\"<br>else<br>&nbsp;&nbsp;&nbsp; # If no timeout, save the output to file<br>&nbsp;&nbsp;&nbsp; echo \"$SNMP_RESULT\" | awk '{print $NF}' &gt; \"$DATA_SNMP_RAIN\"<br>&nbsp;&nbsp;&nbsp; echo \"SNMP rain data saved\"<br>fi<br><br># Check if rain counter increased since last result<br>RAIN_PREV=$(cat \"$DATA_SNMP_RAIN_PREV\")<br>RAIN_NOW=$(cat \"$DATA_SNMP_RAIN\")<br><br>if (( $(echo \"$RAIN_NOW &gt; $RAIN_PREV\" | bc -l) )); then<br>&nbsp;&nbsp;&nbsp; # Output for display<br>&nbsp;&nbsp;&nbsp; RAIN_STATUS=\"Yes\"<br>&nbsp;&nbsp;&nbsp; # Set text colour for display<br>&nbsp;&nbsp;&nbsp; RAIN_COLOUR=\"#49b2fc\"<br>else<br>&nbsp;&nbsp;&nbsp; RAIN_STATUS=\"No\"<br>&nbsp;&nbsp;&nbsp; RAIN_COLOUR=\"#ffffff\"<br>fi<br><br>echo \"Raining: $RAIN_STATUS\"<br>echo \"$RAIN_STATUS\" &gt; \"$DATA_SNMP_RAIN_FLAG\"<br>echo \"$RAIN_COLOUR\" &gt; \"$DATA_SNMP_RAIN_COL\"<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Collect API Data<\/h3>\n\n\n\n<p>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\u2019d found it returning a 403 in the early afternoon when call was set to 30 minutes. Therefore, its set to call every hour.<\/p>\n\n\n\n<p>Cron:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>0 * * * * \/home\/pi\/scripts\/weather_data\/generate_api.sh<\/code><\/pre>\n\n\n\n<p>Script:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>#!\/bin\/bash<br># Pulls latest data from weatherbit api<br><br># File paths<br>DATA_WEATHER_API=\"\/home\/pi\/scripts\/weather_data\/data_api.json\"<br>DATA_API_SUNRISE=\"\/home\/pi\/scripts\/weather_data\/data_api_sunrise.txt\"<br>DATA_API_SUNSET=\"\/home\/pi\/scripts\/weather_data\/data_api_sunset.txt\"<br>DATA_API_DESC=\"\/home\/pi\/scripts\/weather_data\/data_api_desc.txt\"<br>DATA_API_ICON=\"\/home\/pi\/scripts\/weather_data\/data_api_icon.txt\"<br><br># Fetch Data<br>WEATHER_DATA=$(curl -X GET --header 'Accept: application\/json' 'https:\/\/api.weatherbit.io\/v2.0\/current?lat=51.605817&amp;lon=-3.091024&amp;include=alerts&amp;key=00000000000000000000000000000000')<br><br># Check fetch was successful<br>if &#91; $? -ne 0 ]; then<br>&nbsp;&nbsp;&nbsp; echo \"API Call failed\" &gt;&amp;2<br>&nbsp;&nbsp;&nbsp; exit 1<br>else<br>&nbsp;&nbsp;&nbsp; # If successful, write this data to file<br>&nbsp;&nbsp;&nbsp; echo \"$WEATHER_DATA\" &gt; \"$DATA_WEATHER_API\"<br>&nbsp;&nbsp;&nbsp; echo \"Weather data saved\"<br>fi<br><br># Extract data we need<br>SUNRISE=$(jq -r '.data&#91;0].sunrise' &lt; \"$DATA_WEATHER_API\")<br>SUNSET=$(jq -r '.data&#91;0].sunset' &lt; \"$DATA_WEATHER_API\")<br>WEATHER_DESC=$(jq -r '.data&#91;0].weather.description' &lt; \"$DATA_WEATHER_API\")<br>WEATHER_ICON=$(jq -r '.data&#91;0].weather.icon' &lt; \"$DATA_WEATHER_API\")<br><br># API returns UTC time, need to adjust for BST<br># Function to check current date is within the BST period<br>is_bst() {<br>&nbsp;&nbsp;&nbsp; # Use system time to check if we\u2019re in BST<br>&nbsp;&nbsp;&nbsp; &#91;&#91; $(date +%Z) == \"BST\" ]]<br>}<br><br># Function to add 1 hour to the time<br>adjust_for_bst() {<br>&nbsp;&nbsp;&nbsp; local time=\"$1\"<br>&nbsp;&nbsp;&nbsp; date -d \"$time today + 1 hour\" +%H:%M<br>}<br><br># Check if it's currently BST<br>if is_bst; then<br>&nbsp;&nbsp;&nbsp; # Add an hour to sunrise and sunset<br>&nbsp;&nbsp;&nbsp; SUNRISE=$(adjust_for_bst \"$SUNRISE\")<br>&nbsp;&nbsp;&nbsp; SUNSET=$(adjust_for_bst \"$SUNSET\")<br>fi<br><br># Save extracted API data to separate files<br>echo \"$SUNRISE\" &gt; \"$DATA_API_SUNRISE\"<br>echo \"$SUNSET\" &gt; \"$DATA_API_SUNSET\"<br>echo \"$WEATHER_DESC\" &gt; \"$DATA_API_DESC\"<br>echo \"$WEATHER_ICON\" &gt; \"$DATA_API_ICON\"<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Generate HTML<\/h3>\n\n\n\n<p>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.<\/p>\n\n\n\n<p>Like the SDR generation, this is ran every 10 minutes and offset to ensure most recently data is available to display.<\/p>\n\n\n\n<p>Cron:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>3,13,23,33,43,53 * * * * \/home\/pi\/scripts\/weather_data\/generate_html.sh<\/code><\/pre>\n\n\n\n<p>Script:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>#!\/bin\/bash<br># Combines data from radio and api and creates html file for display<br><br># File paths<br>DATA_SNMP_TEMP=\"\/home\/pi\/scripts\/weather_data\/data_snmp_temp.txt\"<br>DATA_SNMP_HUMID=\"\/home\/pi\/scripts\/weather_data\/data_snmp_humid.txt\"<br>#DATA_SNMP_RAIN=\"\/home\/pi\/scripts\/weather_data\/data_snmp_rain.txt\"<br>#DATA_SNMP_RAIN_PREV=\"\/home\/pi\/scripts\/weather_data\/data_snmp_rain_prev.txt\"<br>DATA_SNMP_RAIN_FLAG=\"\/home\/pi\/scripts\/weather_data\/data_snmp_rain_flag.txt\"<br>DATA_SNMP_RAIN_COL=\"\/home\/pi\/scripts\/weather_data\/data_snmp_rain_col.txt\"<br>DATA_WEATHER_API=\"\/home\/pi\/scripts\/weather_data\/data_api.json\"<br>DATA_API_SUNRISE=\"\/home\/pi\/scripts\/weather_data\/data_api_sunrise.txt\"<br>DATA_API_SUNSET=\"\/home\/pi\/scripts\/weather_data\/data_api_sunset.txt\"<br>DATA_API_DESC=\"\/home\/pi\/scripts\/weather_data\/data_api_desc.txt\"<br>DATA_API_ICON=\"\/home\/pi\/scripts\/weather_data\/data_api_icon.txt\"<br>DATA_HTML=\"\/var\/www\/html\/weather.html\"<br><br># Load latest data<br>TEMP=$(cat \"$DATA_SNMP_TEMP\")<br>HUMID=$(cat \"$DATA_SNMP_HUMID\")<br>RAIN=$(cat \"$DATA_SNMP_RAIN_FLAG\")<br>RAIN_COL=$(cat \"$DATA_SNMP_RAIN_COL\")<br>SUNRISE=$(cat \"$DATA_API_SUNRISE\")<br>SUNSET=$(cat \"$DATA_API_SUNSET\")<br>DESC=$(cat \"$DATA_API_DESC\")<br>ICON=$(cat \"$DATA_API_ICON\")<br><br># Generate HTML<br>echo \"Generating HTML content...\"<br>cat &lt;&lt; EOF &gt; \"$DATA_HTML\"<br>&lt;!DOCTYPE html&gt;<br>&lt;html lang=\"en\"&gt;<br>&lt;head&gt;<br>&nbsp;&nbsp;&nbsp; &lt;meta charset=\"UTF-8\"&gt;<br>&nbsp;&nbsp;&nbsp; &lt;meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"&gt;<br>&nbsp;&nbsp;&nbsp; &lt;meta http-equiv=\"refresh\" content=\"60\"&gt; &lt;!-- Refresh every 60 seconds --&gt;<br>&nbsp;&nbsp;&nbsp; &lt;title&gt;Weather Display&lt;\/title&gt;<br>&nbsp;&nbsp;&nbsp; &lt;style&gt;<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; body { font-family: Arial, sans-serif; font-weight: bold; color: white;}<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; .container { width: 100%; margin: 0 auto; text-align: center; }<br>&nbsp;&nbsp;&nbsp; &lt;\/style&gt;<br>&lt;\/head&gt;<br>&lt;body style=\"background-color:black;\"&gt;<br>&nbsp;&nbsp;&nbsp; &lt;div class=\"container\"&gt;<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &lt;table style=\"width:100%\"&gt;&lt;thead&gt;<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; &lt;tr&gt;<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &lt;th colspan=\"2\" rowspan=\"2\"&gt;&lt;img src=\"images\/icons\/$ICON.png\" width=\"120\" height=\"120\"&gt;&lt;\/th&gt;<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &lt;th colspan=\"2\" style=\"text-align: left; font-size: 36px;\"&gt;$DESC&lt;\/th&gt;<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; &lt;\/tr&gt;<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; &lt;tr&gt;<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &lt;th style=\"text-align: right;\"&gt;&lt;img src=\"images\/temperature.png\" width=\"64\" height=\"64\"&gt;&lt;\/th&gt;<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &lt;th style=\"font-size: 56px;\"&gt;${TEMP}C&lt;\/th&gt;<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; &lt;\/tr&gt;&lt;\/thead&gt;<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &lt;tbody&gt;<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; &lt;tr&gt;<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &lt;td&gt;&lt;img src=\"images\/sunrise.png\" width=\"64\" height=\"64\"&gt;&lt;\/td&gt;<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &lt;td style=\"font-size: 48px;\"&gt;$SUNRISE&lt;\/td&gt;<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &lt;td style=\"text-align: right;\"&gt;&lt;img src=\"images\/humidity.png\" width=\"64\" height=\"64\"&gt;&lt;\/td&gt;<br>&nbsp;&nbsp;&nbsp; &nbsp;&nbsp; &lt;td style=\"font-size: 56px;\"&gt;$HUMID%&lt;\/td&gt;<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; &lt;\/tr&gt;<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; &lt;tr&gt;<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &lt;td&gt;&lt;img src=\"images\/sunset.png\" width=\"64\" height=\"64\"&gt;&lt;\/td&gt;<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &lt;td style=\"font-size: 48px;\"&gt;$SUNSET&lt;\/td&gt;<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &lt;td style=\"text-align: right;\"&gt;&lt;img src=\"images\/rain.png\" width=\"64\" height=\"64\"&gt;&lt;\/td&gt;<br>&nbsp;&nbsp;&nbsp; &nbsp;&nbsp; &lt;td style=\"font-size: 56px; color:${RAIN_COL};\"&gt;$RAIN&lt;\/td&gt;<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; &lt;\/tr&gt;<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &lt;\/tbody&gt;<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &lt;\/table&gt;<br>&nbsp;&nbsp;&nbsp; &lt;\/div&gt;<br>&lt;\/body&gt;<br>&lt;\/html&gt;<br>EOF<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Screen Display<\/h3>\n\n\n\n<p>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.<\/p>\n\n\n\n<p>A service would be best, create the file:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/etc\/systemd\/system\/pidisplay.service<\/code><\/pre>\n\n\n\n<p>Add this to the file:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91;Unit]<br>Description=Launches Chromium in kiosk mode for my display.<br><br>&#91;Service]<br>ExecStart=\/home\/pi\/scripts\/display_launch.sh<br>WorkingDirectory=\/home\/pi\/scripts\/<br>User=pi<br>Group=pi<br>Restart=always<br>Environment=DISPLAY=:0<br>Environment=XAUTHORITY=\/home\/pi\/.Xauthority<br><br>&#91;Install]<br>WantedBy=multi-user.target<\/code><\/pre>\n\n\n\n<p>Reload services and enable \/ start the service:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl daemon-reload<br>sudo systemctl enable --now pidisplay.service<\/code><\/pre>\n\n\n\n<p>This launches the display_launch.sh script which in essence loads a Chromium tab in kiosk mode (fullscreen) and with the minimum amount of disruptions:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>#!\/bin\/bash\r\n\r\r\n# Wait for X to start up properly\r\nsleep 10\r\n# Launch Chromium in kiosk mode with the HTML page\r\nexport DISPLAY=:0\r\nchromium-browser --kiosk --disable-restore-session-state --no-sandbox file:\/\/\/home\/pi\/scripts\/display_output.html<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n\n\n\n<p>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.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Update<\/h2>\n\n\n\n<p>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.<\/p>\n\n\n\n<p>One issue that cropped up after a month&#8217;s deployment saw the radio data not being updated anymore. <\/p>\n\n\n\n<p>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.<\/p>\n\n\n\n<p>This is something a cron job to split and archive the output can solve.<\/p>\n\n\n\n<p>Cron:<\/p>\n\n\n\n<p>Run the scricpt every Monday at 00:05.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>5 0 * * 1 \/home\/pi\/433_sensors\/433_json_archive.sh<\/code><\/pre>\n\n\n\n<p>Script:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>#!\/bin\/bash\n\n# Script to cleanup database, leave newest 1M at original filename.\n# Note: If lines are not split cleanly, it'll cause snmp to fail.\n\n# File paths\nORIGINAL_LOG=\"\/home\/pi\/433_sensors\/433_json.txt\"\nRECENT_LOG=\"\/home\/pi\/433_sensors\/temp_recent.txt\"\nLOG_ARCHIVE_DIR=\"\/home\/pi\/433_sensors\/log_archive\"\n\n# Timestamp in format YYYYMMDD-HHMMSS)\nTIMESTAMP=$(date +\"%Y%m%d-%H%M%S\")\n\n# Give a filename for old records (will gzip later)\nOLDER_LOG=\"\/home\/pi\/433_sensors\/433_json_$TIMESTAMP.log\"\n\n# Split last 1000 lines to new file\ntail -n 1000 $ORIGINAL_LOG &gt; $RECENT_LOG\n\n# Split previous lines upto 1000 to another file\nhead -n -1000 $ORIGINAL_LOG &gt; $OLDER_LOG\n\n# Put last 1000 lines back to original filename\nmv $RECENT_LOG $ORIGINAL_LOG\n\n# Compress and move old log to archive dir.\ngzip $OLDER_LOG\nmv \"${OLDER_LOG}.gz\" $LOG_ARCHIVE_DIR<\/code><\/pre>\n\n\n\n<p>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.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>This project is so cobbled together I\u2019m almost proud of it, I don\u2019t 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\u2019d like info on when dawn &hellip; <a href=\"https:\/\/james-batchelor.com\/index.php\/2024\/12\/03\/pi-weather-display-project\/\" class=\"more-link\">Continue reading<span class=\"screen-reader-text\"> &#8220;Pi Weather Display Project&#8221;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[4],"tags":[406,172,63,405,404,407],"class_list":["post-975","post","type-post","status-publish","format-standard","hentry","category-raspberry-pi","tag-api","tag-display","tag-raspberry-pi","tag-sdr","tag-weather","tag-weatherbit-io"],"_links":{"self":[{"href":"https:\/\/james-batchelor.com\/index.php\/wp-json\/wp\/v2\/posts\/975","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/james-batchelor.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/james-batchelor.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/james-batchelor.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/james-batchelor.com\/index.php\/wp-json\/wp\/v2\/comments?post=975"}],"version-history":[{"count":8,"href":"https:\/\/james-batchelor.com\/index.php\/wp-json\/wp\/v2\/posts\/975\/revisions"}],"predecessor-version":[{"id":999,"href":"https:\/\/james-batchelor.com\/index.php\/wp-json\/wp\/v2\/posts\/975\/revisions\/999"}],"wp:attachment":[{"href":"https:\/\/james-batchelor.com\/index.php\/wp-json\/wp\/v2\/media?parent=975"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/james-batchelor.com\/index.php\/wp-json\/wp\/v2\/categories?post=975"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/james-batchelor.com\/index.php\/wp-json\/wp\/v2\/tags?post=975"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}