Gemini Weather Channel

2024 01 01

Here are some notes about the little weather station that is running on my Gemini capsule.

Hardware:

a Raspberry Pi Zero W

a BME680 Breakout board

The Pi is within an enclosure with the BME680 hanging off it via a ribbon cable. The assembly is mounted outside but in a relatively sheltered position on a covered balcony. From there it is relatively well protected from the elements whilst still being able to sip from the delightfully tepid and chunky London city air.

To do this, a cron job (take_measurements.py) runs every ten minutes to capture the current temperature, pressure, and humidity. The breakout can also measure "air quality" but it takes some time (about 20 minutes) for the sensor to stablise so I do not use it. The cron job involves running a simple Python script using the bme680 module to take the readings and then write them to a text file. The script appends new readings into a single text file which gradually accumulates data, one timepoint per line and up to 150 readings, i.e. readings from the previous 25 hours. I decided to keep 25 hours as it made thhe subsequent charts showing the 'previous day' easier to read.

A second cron job, offset from the measurement-taking job by two minutes, runs plot_measurements.py to load data from the file, calculate some basic stats, and create the plots. The text-based plots are handled by the termplotlib module and everything is written to a .gmi text file, with the appropriate Gemini formatting. The output file is then rsync'd to the appropriate location on the Gemini server so that you can see it.

The two Python scripts are below:

take_measurements.py

#!/usr/bin/env python3
#-*- coding: utf-8 -*-

import bme680
from datetime import datetime

do_gas = False
outfile = '/home/pi/weather_log.txt'

try:
    sensor = bme680.BME680(bme680.I2C_ADDR_PRIMARY)
except (RuntimeError, IOError):
    sensor = bme680.BME680(bme680.I2C_ADDR_SECONDARY)

# set a balance between accuracy of reading and
# amount of noise. The higher the oversampling,
# the greater the reduction in noise, albeit with
# a loss of accuracy.
sensor.set_humidity_oversample(bme680.OS_2X)
sensor.set_pressure_oversample(bme680.OS_4X)
sensor.set_temperature_oversample(bme680.OS_8X)
sensor.set_filter(bme680.FILTER_SIZE_3)

# gas sensor settings
if do_gas:
    sensor.set_gas_status(bme680.ENABLE_GAS_MEAS)
    sensor.set_gas_heater_temperature(320)
    sensor.set_gas_heater_duration(150)
    sensor.select_gas_heater_profile(0)

timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

# temp, pressure, humidity
output = "{0:.2f},{1:.2f},{2:.2f}".format(sensor.data.temperature, sensor.data.pressure, sensor.data.humidity)

#if do_gas and sensor.data.heat_stable:
#    print("{0},{1} Ohms".format(output, sensor.data.gas_resistance))

with open(outfile, 'r') as file_in:
    data = file_in.read().splitlines(True)

data.append(timestamp + ',' + output + '\n')

with open(outfile, 'w') as file_out:
    file_out.writelines(data[-150:])

plot_measurements.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import numpy as np
from csv import reader
import termplotlib as tpl
from datetime import datetime

input_file = '/home/dave/weather_log.txt'

def get_trend(curr_num, avrg_num, norm_range, sensitivity):
    curr_normd = (curr_num - norm_range[0]) / (norm_range[1] - norm_range[0])
    avrg_normd = (avrg_num - norm_range[0]) / (norm_range[1] - norm_range[0])

    ratio = curr_normd / avrg_normd
    thr_upper = (100 + sensitivity) / 100
    thr_lower = (100 - sensitivity) / 100

    if ratio > thr_upper:
        ret_trend = 'increasing'
    elif ratio < thr_lower:
        ret_trend = 'decreasing'
    else:
        ret_trend = 'unchanged'

    return ret_trend

data_times = []
data_temp = []
data_press = []
data_relhum = []

# load the data
with open(input_file, "r") as my_file:
    file_reader = reader(my_file)
    for i in file_reader:
        data_times.append(datetime.strptime(i[0], '%Y-%m-%d %H:%M:%S'))
        data_temp.append(float(i[1]))
        data_press.append(float(i[2]))
        data_relhum.append(float(i[3]))

# calculate the past 60 minutes average for each measurement
avg_temp = sum(data_temp[-7:-1]) / 6
avg_pres = sum(data_press[-7:-1]) / 6
avg_rhum = sum(data_relhum[-7:-1]) / 6

cur_temp = data_temp[-1]
cur_pres = data_press[-1]
cur_rhum = data_relhum[-1]

dir_temp = get_trend(cur_temp, avg_temp, [min(data_temp), max(data_temp)], 5)
dir_pres = get_trend(cur_pres, avg_pres, [min(data_press), max(data_press)], 10)
dir_rhum = get_trend(cur_rhum, avg_rhum, [min(data_relhum), max(data_relhum)], 15)

# write GemText output ...

print('# Local Weather Report')
print('## London, UK')
print('')
print('=> gemini://bleyble.com/users/quokka/textcam/index.gmi 📷 Measurements are taken from the same location at the TextCam.')
print('=> gemini://bleyble.com/users/quokka/index.gmi ⬑ Go back...')
print('')
print('### Most Recent Measurements, taken ' + str(data_times[-1]) + ' (UTC)')
print('')
print('```')
print('Measurement \tValue \tUnits \tTrend \t\tMin. \tMax.')
print('------------\t------\t------\t------\t\t----\t----')
print('Temperature \t' + str(cur_temp) + '\t°C \t' + dir_temp + ' \t' + str(min(data_temp)) + ' \t' + str(max(data_temp)))
print('Air Pressure \t' + str(cur_pres) + '\tmbar \t' + dir_pres + ' \t' + str(min(data_press)) + '\t' + str(max(data_press)))
print('Rel.Humidity \t' + str(cur_rhum) + '\t% \t' + dir_rhum + ' \t' + str(min(data_relhum)) + ' \t' + str(max(data_relhum)))
print('------------\t-----\t-----\t------\t\t----\t----')
print('```')
print('')

print('### Charts - Past Day')
x = np.linspace(-25,0,150)

ymin = int(np.floor(min(data_temp)/5)*5)
ymax = int(np.ceil(max(data_temp)/5)*5)

print('```')
fig = tpl.figure()
fig.plot(x, data_temp, width=75, height=30, xlim=[-26,1], ylim=[ymin,ymax], title='Temperature, last 25 hours')
fig.show()
print('\t\tHours')
print('```')

print('')
print('')

ymin = int(np.floor(min(data_press)/2)*2)
ymax = int(np.ceil(max(data_press)/2)*2)

print('```')
fig = tpl.figure()
fig.plot(x, data_press, width=80, height=30, xlim=[-25,1], ylim=[ymin,ymax], title='Pressure, last 25 hours')
fig.show()
print('\t\tHours')
print('```')

print('')
print('')

ymin = int(np.floor(min(data_relhum)/10)*10)
ymax = int(np.ceil(max(data_relhum)/10)*10)

print('```')
fig = tpl.figure()
fig.plot(x, data_relhum, width=80, height=30, xlim=[-25,1], ylim=[ymin,ymax], title='Relative Humidity, last 25 hours',
         extra_gnuplot_arguments='set xlabel')
fig.show()
print('\t\tHours')
print('```')
print('')
print('=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-')
print('')
print('=> gemini://bleyble.com/users/quokka/index.gmi ⬑ Go back up a level')
print('=> gemini://bleyble.com/users/quokka/index.gmi ↂ Quokka\'s Capsule')
print('=> gemini://bleyble.com/index.gmi ↯ bleyble.com')
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

⬑ Back to the glog index

ↂ Quokka's Capsule

↯ bleyble.com

~EOF~