Description
This project is a Raspberry Pi-based weather station that collects temperature, pressure, and humidity data using a BME280 environmental sensor. Readings are streamed over the serial port, logged, and pushed to Github for long-term storage.
A Python-based serial logger runs on the RPi, capturing sensor output once per minute, timestamping each entry, and organizing logs into a hierarchical directory structure (year / month / week / day). Log files are committed and pushed to a dedicated GitHub repository every 30 minutes, enabling reliable archival and remote access to data.
Technical Overview
Hardware Components:
1× Raspberry Pi Zero 2 W
1× BME280 Temperature / Pressure / Humidity Sensor
Pin Connections:
BME280 3V3 → RPi Pin 1
BME280 GND → RPi Pin 6
BME280 SDA → RPi Pin 3
BME280 SCL → RPi Pin 5
System Responsibilities:
- The BME280 reads environmental data.
- The Raspberri Pi 02W, running a Python script, checks the serial port for data and adds it to a log.
- GitHub acts as a remote data archive for long-term storage.
Python Code
Click to expand
#!/usr/bin/env python3
import os
import time
import socket
import tarfile
import shutil
from datetime import datetime, timedelta
import board
import busio
import adafruit_bme280.basic as bme280
from pathlib import Path
import subprocess
# ----------------------
# CONFIGURATION
# ----------------------
LOG_BASE = Path.home() / "RPi_Compact-Weather-Station" / "logs"
LOG_INTERVAL = 60 # seconds between sensor reads
GIT_PUSH_INTERVAL_MINUTES = 30 # push at every :00 and :30
ARCHIVE_DAYS = 30
CSV_HEADER = "temp_c,temp_f,pressure_hpa,humidity"
# ----------------------
# INITIALIZE SENSOR
# ----------------------
i2c = busio.I2C(board.SCL, board.SDA)
sensor = bme280.Adafruit_BME280_I2C(i2c, address=0x76)
sensor.sea_level_pressure = 1013.25
# ----------------------
# HELPER FUNCTIONS
# ----------------------
def is_online(host="github.com", port=443, timeout=5):
try:
socket.create_connection((host, port), timeout=timeout)
return True
except OSError:
return False
def ensure_log_file(path: Path):
if not path.exists():
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, "w") as f:
f.write(CSV_HEADER + "\n")
def get_log_path() -> Path:
now = datetime.now()
year = str(now.year)
month = now.strftime("%m_%B")
week = f"Week_{now.isocalendar()[1]:02d}"
filename = now.strftime("%Y-%m-%d.log")
return LOG_BASE / year / month / week / filename
def log_alert(message: str):
now = datetime.now()
year = str(now.year)
month = now.strftime("%m_%B")
week = f"Week_{now.isocalendar()[1]:02d}"
folder = LOG_BASE / year / month / week
folder.mkdir(parents=True, exist_ok=True)
alert_file = folder / "ALERTS.log"
timestamp = now.isoformat(timespec="seconds")
with open(alert_file, "a") as f:
f.write(f"{timestamp} {message}\n")
print(f"[ALERT] {message}")
def log_data(logfile: Path, line: str):
timestamp = datetime.now().isoformat(timespec="seconds")
with open(logfile, "a") as f:
f.write(f"{timestamp} {line}\n")
log_alert(f"[DATA LOGGED] {line}")
def push_git():
now = datetime.now()
if not is_online():
log_alert(f"[GIT PUSH SKIPPED] Offline. Will retry at next interval ({now.strftime('%Y-%m-%d %H:%M')})")
return
try:
subprocess.run(["git", "-C", str(LOG_BASE.parent), "add", "."], check=True)
subprocess.run([
"git", "-C", str(LOG_BASE.parent),
"commit", "-m",
f"Automated log update {now.strftime('%Y-%m-%d %H:%M')}"
], check=True)
subprocess.run(["git", "-C", str(LOG_BASE.parent), "push"], check=True)
log_alert(f"[GIT PUSH] Successful at {now.strftime('%Y-%m-%d %H:%M')}")
except subprocess.CalledProcessError as e:
log_alert(f"[GIT PUSH ERROR] {e}. Will retry next interval.")
def compress_old_weeks():
cutoff = datetime.now() - timedelta(days=ARCHIVE_DAYS)
for year_dir in LOG_BASE.iterdir():
if not year_dir.is_dir():
continue
for month_dir in year_dir.iterdir():
if not month_dir.is_dir():
continue
for week_dir in month_dir.iterdir():
if not week_dir.is_dir():
continue
log_files = sorted([f for f in week_dir.iterdir() if f.suffix == ".log"])
if not log_files:
continue
last_log_file = log_files[-1]
try:
last_log_date = datetime.strptime(last_log_file.stem, "%Y-%m-%d")
except ValueError:
continue
if last_log_date < cutoff:
archive_name = str(week_dir) + ".tar.gz"
with tarfile.open(archive_name, "w:gz") as tar:
for f in week_dir.iterdir():
tar.add(f, arcname=f.name)
shutil.rmtree(week_dir)
log_alert(f"[ARCHIVE] Compressed week folder {week_dir} -> {archive_name}")
# ----------------------
# MAIN LOOP
# ----------------------
log_alert("Weather logger started... Press Ctrl+C to stop.")
last_git_push_time = None
next_push = lambda: (datetime.now().replace(minute=(0 if datetime.now().minute < 30 else 30),
second=0, microsecond=0))
log_alert(f"Next Git push scheduled at: {next_push().strftime('%Y-%m-%d %H:%M')}")
try:
last_log_monotonic = time.monotonic()
while True:
# ----------------------
# Sensor read & log
# ----------------------
now_monotonic = time.monotonic()
if now_monotonic - last_log_monotonic >= LOG_INTERVAL:
last_log_monotonic = now_monotonic
logfile = get_log_path()
ensure_log_file(logfile)
temp_c = round(sensor.temperature, 2)
temp_f = round(temp_c * 1.8 + 32, 2)
pressure = round(sensor.pressure, 2)
humidity = round(sensor.humidity, 2)
line = f"{temp_c},{temp_f},{pressure},{humidity}"
log_data(logfile, line)
# ----------------------
# Compress old weeks
# ----------------------
compress_old_weeks()
# ----------------------
# Git push every :00/:30
# ----------------------
now = datetime.now()
interval_id = now.strftime("%Y-%m-%d_%H_%M")
if now.minute % 30 == 0 and interval_id != last_git_push_time:
last_git_push_time = interval_id
push_git()
# ----------------------
# Sleep small interval for efficiency
# ----------------------
time.sleep(1)
except KeyboardInterrupt:
log_alert("Logger stopped by user.")