Learn how to build your own weather station using Raspberry Pi 4 with temperature, humidity, pressure sensors and web dashboard. Complete beginner tutorial with code examples, 3D printed enclosure, and data logging.
Keywords: raspberry pi weather station, diy weather station, home weather monitoring, raspberry pi sensors, weather station project, arduino weather station alternative, outdoor weather station raspberry pi
Create your own professional-grade weather station that measures temperature, humidity, barometric pressure, and more. Display real-time data on a web dashboard, log historical trends, and even set up weather alerts—all for under $100.
What You'll Build
A complete weather monitoring system featuring:
- Real-time measurements - Temperature, humidity, pressure, light levels
- Web dashboard - Beautiful charts and current conditions
- Data logging - Store months of historical weather data
- Weather alerts - Email/SMS notifications for extreme conditions
- API access - Share data with weather networks or apps
- Mobile-friendly - Check conditions from your phone
- Solar powered option - Off-grid weather monitoring
- Expandable - Add wind, rain, and air quality sensors
Difficulty: ⭐⭐⭐ Intermediate | Time Required: 3-5 hours | Cost: $60-120
What You'll Learn
- Interfacing sensors with Raspberry Pi GPIO pins
- I2C communication protocol for digital sensors
- Python programming for data collection
- Database setup and management (SQLite)
- Web development with Flask framework
- Data visualization with charts and graphs
- 3D printing and weatherproof enclosure design
- Solar power system design for outdoor deployment
Required Components
Raspberry Pi Setup
- Raspberry Pi 4 (4GB) – Best performance for web interface
- Raspberry Pi 3 B+ – Budget option, still excellent
- SanDisk 128GB microSD Card – Plenty of space for data logging
- Pi 4 Case – Indoor electronics protection
Essential Sensors
- BME280 sensor ($10-15) - Temperature, humidity, pressure in one chip
- TSL2591 light sensor ($8-12) - Ambient light measurement
- Jumper wires ($5) - Connect sensors to Pi
- Breadboard ($5) - Prototype connections
Weatherproof Enclosure
- Waterproof project box ($15-25) - IP65 rated minimum
- Cable glands ($5) - Sealed cable entry points
- Silica gel packets ($3) - Prevent condensation
- Solar radiation shield ($10) - Protect sensors from direct sun
Optional Enhancements
- DS18B20 temperature sensor ($5) - Outdoor temperature probe
- Rain gauge sensor ($20-30) - Measure precipitation
- Wind speed/direction sensor ($30-50) - Anemometer and vane
- Air quality sensor ($15-25) - PM2.5 and PM10 particles
- Solar panel + battery ($40-60) - Off-grid power system
Tools Needed
- Soldering iron and solder (for permanent connections)
- Drill and bits (for enclosure mounting)
- Multimeter (for troubleshooting)
- 3D printer (optional, for custom parts)
Weather Station Design Overview
Sensor Selection Guide
BME280 (Temperature, Humidity, Pressure):
- Range: -40°C to 85°C, 0-100% RH, 300-1100 hPa
- Accuracy: ±1°C, ±3% RH, ±1 hPa
- Interface: I2C or SPI
- Why choose: All-in-one, accurate, widely supported
TSL2591 (Light Sensor):
- Range: 188 µLux to 88,000 Lux
- Features: IR and full spectrum measurement
- Interface: I2C
- Why choose: Weather-resistant, excellent dynamic range
DS18B20 (Waterproof Temperature):
- Range: -55°C to 125°C
- Accuracy: ±0.5°C
- Interface: 1-Wire (single data pin)
- Why choose: Waterproof probe, multiple sensors on one pin
System Architecture
Data Flow:
- Sensors → Raspberry Pi GPIO/I2C
- Python script → Reads sensors every minute
- SQLite database → Stores timestamped measurements
- Flask web server → Provides dashboard interface
- Charts.js → Visualizes data in browser
File Structure:
weather_station/
├── weather_station.py # Main data collection script
├── web_dashboard.py # Flask web interface
├── database.py # Database operations
├── sensors.py # Sensor interface classes
├── templates/
│ └── dashboard.html # Web dashboard template
├── static/
│ ├── style.css # Dashboard styling
│ └── charts.js # Data visualization
└── weather_data.db # SQLite database file
Step-by-Step Build Guide
Step 1: Set Up Raspberry Pi
Install Raspberry Pi OS:
- Use Raspberry Pi Imager
- Choose: Raspberry Pi OS (64-bit)
- Enable SSH and set credentials
- Configure Wi-Fi if not using ethernet
- Flash to microSD card
Boot and initial setup:
# SSH into Pi
ssh pi@raspberrypi.local
# Update system
sudo apt update && sudo apt upgrade -y
# Enable I2C interface
sudo raspi-config
# Interface Options → I2C → Enable
# Install required packages
sudo apt install python3-pip python3-venv git -y
Step 2: Install Python Libraries
Create project directory:
mkdir ~/weather_station
cd ~/weather_station
python3 -m venv venv
source venv/bin/activate
Install sensor libraries:
pip install adafruit-circuitpython-bme280
pip install adafruit-circuitpython-tsl2591
pip install w1thermsensor
pip install flask
pip install sqlite3
pip install requests
Step 3: Wire the Sensors
BME280 connections:
- VCC → 3.3V (Pin 1)
- GND → Ground (Pin 6)
- SCL → GPIO 3 (Pin 5)
- SDA → GPIO 2 (Pin 3)
TSL2591 connections:
- VCC → 3.3V (Pin 1)
- GND → Ground (Pin 14)
- SCL → GPIO 3 (Pin 5) - shared with BME280
- SDA → GPIO 2 (Pin 3) - shared with BME280
DS18B20 connections (if using):
- Red → 3.3V (Pin 1)
- Black → Ground (Pin 9)
- Yellow → GPIO 4 (Pin 7)
- 4.7kΩ resistor between data and power
Step 4: Test Individual Sensors
Test BME280:
# test_bme280.py
import board
import adafruit_bme280
i2c = board.I2C()
bme280 = adafruit_bme280.Adafruit_BME280_I2C(i2c)
print(f"Temperature: {bme280.temperature:.1f} °C")
print(f"Humidity: {bme280.relative_humidity:.1f} %")
print(f"Pressure: {bme280.pressure:.1f} hPa")
Test TSL2591:
# test_tsl2591.py
import board
import adafruit_tsl2591
i2c = board.I2C()
sensor = adafruit_tsl2591.TSL2591(i2c)
print(f"Lux: {sensor.lux}")
print(f"Infrared: {sensor.infrared}")
print(f"Visible: {sensor.visible}")
Test DS18B20:
# test_ds18b20.py
from w1thermsensor import W1ThermSensor
sensor = W1ThermSensor()
temperature = sensor.get_temperature()
print(f"Outdoor Temperature: {temperature:.1f} °C")
Step 5: Create Sensor Interface Classes
# sensors.py
import board
import adafruit_bme280
import adafruit_tsl2591
from w1thermsensor import W1ThermSensor
import time
class WeatherSensors:
def __init__(self):
"""Initialize all sensors"""
self.i2c = board.I2C()
# BME280 for indoor conditions
try:
self.bme280 = adafruit_bme280.Adafruit_BME280_I2C(self.i2c)
print("BME280 initialized successfully")
except Exception as e:
print(f"BME280 initialization failed: {e}")
self.bme280 = None
# TSL2591 for light measurement
try:
self.tsl2591 = adafruit_tsl2591.TSL2591(self.i2c)
print("TSL2591 initialized successfully")
except Exception as e:
print(f"TSL2591 initialization failed: {e}")
self.tsl2591 = None
# DS18B20 for outdoor temperature
try:
self.ds18b20 = W1ThermSensor()
print("DS18B20 initialized successfully")
except Exception as e:
print(f"DS18B20 initialization failed: {e}")
self.ds18b20 = None
def read_all_sensors(self):
"""Read all sensors and return data dictionary"""
data = {
'timestamp': time.time(),
'indoor_temp': None,
'outdoor_temp': None,
'humidity': None,
'pressure': None,
'light_lux': None,
'light_ir': None,
'light_visible': None
}
# Read BME280
if self.bme280:
try:
data['indoor_temp'] = round(self.bme280.temperature, 2)
data['humidity'] = round(self.bme280.relative_humidity, 2)
data['pressure'] = round(self.bme280.pressure, 2)
except Exception as e:
print(f"BME280 read error: {e}")
# Read TSL2591
if self.tsl2591:
try:
data['light_lux'] = round(self.tsl2591.lux, 2)
data['light_ir'] = self.tsl2591.infrared
data['light_visible'] = self.tsl2591.visible
except Exception as e:
print(f"TSL2591 read error: {e}")
# Read DS18B20
if self.ds18b20:
try:
data['outdoor_temp'] = round(self.ds18b20.get_temperature(), 2)
except Exception as e:
print(f"DS18B20 read error: {e}")
return data
def get_sensor_status(self):
"""Return status of all sensors"""
return {
'bme280': self.bme280 is not None,
'tsl2591': self.tsl2591 is not None,
'ds18b20': self.ds18b20 is not None
}
Step 6: Create Database Manager
# database.py
import sqlite3
import json
from datetime import datetime, timedelta
class WeatherDatabase:
def __init__(self, db_path='weather_data.db'):
self.db_path = db_path
self.init_database()
def init_database(self):
"""Create database tables if they don't exist"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS weather_data (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp REAL NOT NULL,
indoor_temp REAL,
outdoor_temp REAL,
humidity REAL,
pressure REAL,
light_lux REAL,
light_ir INTEGER,
light_visible INTEGER,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)
''')
conn.commit()
conn.close()
print("Database initialized successfully")
def insert_reading(self, data):
"""Insert a weather reading into the database"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
INSERT INTO weather_data
(timestamp, indoor_temp, outdoor_temp, humidity, pressure,
light_lux, light_ir, light_visible)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
''', (
data['timestamp'],
data['indoor_temp'],
data['outdoor_temp'],
data['humidity'],
data['pressure'],
data['light_lux'],
data['light_ir'],
data['light_visible']
))
conn.commit()
conn.close()
def get_recent_readings(self, hours=24):
"""Get readings from the last N hours"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
since_timestamp = datetime.now().timestamp() - (hours * 3600)
cursor.execute('''
SELECT * FROM weather_data
WHERE timestamp > ?
ORDER BY timestamp DESC
''', (since_timestamp,))
columns = [desc[0] for desc in cursor.description]
results = [dict(zip(columns, row)) for row in cursor.fetchall()]
conn.close()
return results
def get_current_conditions(self):
"""Get the most recent reading"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
SELECT * FROM weather_data
ORDER BY timestamp DESC
LIMIT 1
''')
row = cursor.fetchone()
if row:
columns = [desc[0] for desc in cursor.description]
result = dict(zip(columns, row))
else:
result = None
conn.close()
return result
def get_statistics(self, hours=24):
"""Get min/max/avg statistics"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
since_timestamp = datetime.now().timestamp() - (hours * 3600)
cursor.execute('''
SELECT
MIN(indoor_temp) as min_indoor_temp,
MAX(indoor_temp) as max_indoor_temp,
AVG(indoor_temp) as avg_indoor_temp,
MIN(outdoor_temp) as min_outdoor_temp,
MAX(outdoor_temp) as max_outdoor_temp,
AVG(outdoor_temp) as avg_outdoor_temp,
MIN(humidity) as min_humidity,
MAX(humidity) as max_humidity,
AVG(humidity) as avg_humidity,
MIN(pressure) as min_pressure,
MAX(pressure) as max_pressure,
AVG(pressure) as avg_pressure
FROM weather_data
WHERE timestamp > ?
''', (since_timestamp,))
row = cursor.fetchone()
if row:
columns = [desc[0] for desc in cursor.description]
result = dict(zip(columns, row))
# Round all values to 2 decimal places
for key, value in result.items():
if value is not None:
result[key] = round(value, 2)
else:
result = None
conn.close()
return result
Step 7: Create Main Data Collection Script
# weather_station.py
#!/usr/bin/env python3
import time
import logging
from datetime import datetime
from sensors import WeatherSensors
from database import WeatherDatabase
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('weather_station.log'),
logging.StreamHandler()
]
)
class WeatherStation:
def __init__(self):
self.sensors = WeatherSensors()
self.database = WeatherDatabase()
self.reading_interval = 60 # seconds between readings
def run_continuous(self):
"""Run continuous data collection"""
logging.info("Weather station started")
while True:
try:
# Read all sensors
data = self.sensors.read_all_sensors()
# Log the reading
logging.info(f"Reading: T={data['indoor_temp']}°C, "
f"H={data['humidity']}%, "
f"P={data['pressure']}hPa, "
f"L={data['light_lux']}lux")
# Store in database
self.database.insert_reading(data)
# Check for alerts
self.check_alerts(data)
# Wait for next reading
time.sleep(self.reading_interval)
except KeyboardInterrupt:
logging.info("Weather station stopped by user")
break
except Exception as e:
logging.error(f"Error in main loop: {e}")
time.sleep(5) # Wait before retrying
def check_alerts(self, data):
"""Check for alert conditions"""
alerts = []
# Temperature alerts
if data['indoor_temp'] and data['indoor_temp'] > 30:
alerts.append(f"High indoor temperature: {data['indoor_temp']}°C")
if data['outdoor_temp'] and data['outdoor_temp'] < -10:
alerts.append(f"Very cold outdoor temperature: {data['outdoor_temp']}°C")
# Humidity alerts
if data['humidity'] and data['humidity'] > 70:
alerts.append(f"High humidity: {data['humidity']}%")
# Pressure alerts (rapid changes indicate weather changes)
if data['pressure'] and data['pressure'] < 990:
alerts.append(f"Low pressure (storm approaching?): {data['pressure']}hPa")
# Log alerts
for alert in alerts:
logging.warning(f"ALERT: {alert}")
# TODO: Send email/SMS notifications
def get_status(self):
"""Get system status"""
sensor_status = self.sensors.get_sensor_status()
current_data = self.database.get_current_conditions()
return {
'sensors': sensor_status,
'current_conditions': current_data,
'uptime': time.time()
}
if __name__ == "__main__":
station = WeatherStation()
station.run_continuous()
Step 8: Create Web Dashboard
# web_dashboard.py
from flask import Flask, render_template, jsonify
import json
from datetime import datetime
from database import WeatherDatabase
from sensors import WeatherSensors
app = Flask(__name__)
db = WeatherDatabase()
sensors = WeatherSensors()
@app.route('/')
def dashboard():
"""Main dashboard page"""
return render_template('dashboard.html')
@app.route('/api/current')
def api_current():
"""Get current weather conditions"""
current = db.get_current_conditions()
if current:
# Convert timestamp to readable format
current['datetime'] = datetime.fromtimestamp(current['timestamp']).strftime('%Y-%m-%d %H:%M:%S')
return jsonify(current)
else:
return jsonify({'error': 'No data available'})
@app.route('/api/history/<int:hours>')
def api_history(hours):
"""Get historical data for the last N hours"""
data = db.get_recent_readings(hours)
# Format data for charts
chart_data = {
'timestamps': [],
'indoor_temp': [],
'outdoor_temp': [],
'humidity': [],
'pressure': [],
'light_lux': []
}
for reading in reversed(data): # Reverse to get chronological order
dt = datetime.fromtimestamp(reading['timestamp'])
chart_data['timestamps'].append(dt.strftime('%H:%M'))
chart_data['indoor_temp'].append(reading['indoor_temp'])
chart_data['outdoor_temp'].append(reading['outdoor_temp'])
chart_data['humidity'].append(reading['humidity'])
chart_data['pressure'].append(reading['pressure'])
chart_data['light_lux'].append(reading['light_lux'])
return jsonify(chart_data)
@app.route('/api/statistics/<int:hours>')
def api_statistics(hours):
"""Get statistics for the last N hours"""
stats = db.get_statistics(hours)
return jsonify(stats)
@app.route('/api/status')
def api_status():
"""Get system status"""
sensor_status = sensors.get_sensor_status()
return jsonify({
'sensors': sensor_status,
'timestamp': datetime.now().isoformat()
})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)
Step 9: Create Web Dashboard Template
<!-- templates/dashboard.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Weather Station Dashboard</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<div class="container">
<header>
<h1>🌤️ Personal Weather Station</h1>
<div id="last-update"></div>
</header>
<div class="current-conditions">
<h2>Current Conditions</h2>
<div class="conditions-grid">
<div class="condition-card">
<h3>🌡️ Indoor Temperature</h3>
<div class="value" id="indoor-temp">--°C</div>
</div>
<div class="condition-card">
<h3>🌡️ Outdoor Temperature</h3>
<div class="value" id="outdoor-temp">--°C</div>
</div>
<div class="condition-card">
<h3>💧 Humidity</h3>
<div class="value" id="humidity">--%</div>
</div>
<div class="condition-card">
<h3>🌬️ Pressure</h3>
<div class="value" id="pressure">-- hPa</div>
</div>
<div class="condition-card">
<h3>☀️ Light Level</h3>
<div class="value" id="light">-- lux</div>
</div>
</div>
</div>
<div class="statistics">
<h2>24-Hour Statistics</h2>
<div class="stats-grid">
<div class="stat-card">
<h4>Temperature Range</h4>
<div>High: <span id="temp-high">--°C</span></div>
<div>Low: <span id="temp-low">--°C</span></div>
</div>
<div class="stat-card">
<h4>Humidity Range</h4>
<div>High: <span id="humidity-high">--%</span></div>
<div>Low: <span id="humidity-low">--%</span></div>
</div>
<div class="stat-card">
<h4>Pressure Range</h4>
<div>High: <span id="pressure-high">-- hPa</span></div>
<div>Low: <span id="pressure-low">-- hPa</span></div>
</div>
</div>
</div>
<div class="charts">
<h2>Historical Data</h2>
<div class="chart-controls">
<button onclick="loadChart(6)" class="active">6 Hours</button>
<button onclick="loadChart(24)">24 Hours</button>
<button onclick="loadChart(168)">7 Days</button>
</div>
<div class="chart-container">
<canvas id="temperatureChart"></canvas>
</div>
<div class="chart-container">
<canvas id="humidityChart"></canvas>
</div>
<div class="chart-container">
<canvas id="pressureChart"></canvas>
</div>
</div>
<div class="system-status">
<h2>System Status</h2>
<div id="sensor-status"></div>
</div>
</div>
<script src="{{ url_for('static', filename='dashboard.js') }}"></script>
</body>
</html>
Step 10: Create Dashboard Styling and JavaScript
/* static/style.css */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #333;
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
header {
text-align: center;
color: white;
margin-bottom: 30px;
}
header h1 {
font-size: 2.5rem;
margin-bottom: 10px;
}
#last-update {
font-size: 1rem;
opacity: 0.9;
}
.current-conditions, .statistics, .charts, .system-status {
background: white;
border-radius: 15px;
padding: 25px;
margin-bottom: 30px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
}
.conditions-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-top: 20px;
}
.condition-card {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
padding: 20px;
border-radius: 10px;
text-align: center;
}
.condition-card h3 {
font-size: 1rem;
margin-bottom: 10px;
color: #666;
}
.condition-card .value {
font-size: 2rem;
font-weight: bold;
color: #333;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-top: 20px;
}
.stat-card {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
border-left: 4px solid #667eea;
}
.chart-controls {
margin: 20px 0;
text-align: center;
}
.chart-controls button {
background: #667eea;
color: white;
border: none;
padding: 10px 20px;
margin: 0 5px;
border-radius: 5px;
cursor: pointer;
transition: background 0.3s;
}
.chart-controls button:hover {
background: #5a67d8;
}
.chart-controls button.active {
background: #4c51bf;
}
.chart-container {
margin: 20px 0;
height: 300px;
}
.system-status {
text-align: center;
}
.sensor-online {
color: #48bb78;
}
.sensor-offline {
color: #f56565;
}
@media (max-width: 768px) {
.conditions-grid {
grid-template-columns: 1fr;
}
header h1 {
font-size: 2rem;
}
.chart-container {
height: 250px;
}
}
// static/dashboard.js
let temperatureChart, humidityChart, pressureChart;
// Initialize dashboard
document.addEventListener('DOMContentLoaded', function() {
loadCurrentConditions();
loadStatistics();
loadSystemStatus();
loadChart(6); // Load 6-hour chart by default
// Refresh data every 30 seconds
setInterval(function() {
loadCurrentConditions();
loadStatistics();
loadSystemStatus();
}, 30000);
});
function loadCurrentConditions() {
fetch('/api/current')
.then(response => response.json())
.then(data => {
if (data.error) {
console.error('Error loading current conditions:', data.error);
return;
}
document.getElementById('indoor-temp').textContent =
data.indoor_temp ? data.indoor_temp + '°C' : '--°C';
document.getElementById('outdoor-temp').textContent =
data.outdoor_temp ? data.outdoor_temp + '°C' : '--°C';
document.getElementById('humidity').textContent =
data.humidity ? data.humidity + '%' : '--%';
document.getElementById('pressure').textContent =
data.pressure ? data.pressure + ' hPa' : '-- hPa';
document.getElementById('light').textContent =
data.light_lux ? data.light_lux + ' lux' : '-- lux';
document.getElementById('last-update').textContent =
'Last updated: ' + data.datetime;
})
.catch(error => {
console.error('Error fetching current conditions:', error);
});
}
function loadStatistics() {
fetch('/api/statistics/24')
.then(response => response.json())
.then(data => {
document.getElementById('temp-high').textContent =
data.max_indoor_temp ? data.max_indoor_temp + '°C' : '--°C';
document.getElementById('temp-low').textContent =
data.min_indoor_temp ? data.min_indoor_temp + '°C' : '--°C';
document.getElementById('humidity-high').textContent =
data.max_humidity ? data.max_humidity + '%' : '--%';
document.getElementById('humidity-low').textContent =
data.min_humidity ? data.min_humidity + '%' : '--%';
document.getElementById('pressure-high').textContent =
data.max_pressure ? data.max_pressure + ' hPa' : '-- hPa';
document.getElementById('pressure-low').textContent =
data.min_pressure ? data.min_pressure + ' hPa' : '-- hPa';
})
.catch(error => {
console.error('Error fetching statistics:', error);
});
}
function loadSystemStatus() {
fetch('/api/status')
.then(response => response.json())
.then(data => {
let statusHtml = '<h3>Sensor Status</h3>';
statusHtml += '<p>BME280 (Temp/Humidity/Pressure): ';
statusHtml += data.sensors.bme280 ?
'<span class="sensor-online">Online</span>' :
'<span class="sensor-offline">Offline</span>';
statusHtml += '</p>';
statusHtml += '<p>TSL2591 (Light): ';
statusHtml += data.sensors.tsl2591 ?
'<span class="sensor-online">Online</span>' :
'<span class="sensor-offline">Offline</span>';
statusHtml += '</p>';
statusHtml += '<p>DS18B20 (Outdoor Temp): ';
statusHtml += data.sensors.ds18b20 ?
'<span class="sensor-online">Online</span>' :
'<span class="sensor-offline">Offline</span>';
statusHtml += '</p>';
document.getElementById('sensor-status').innerHTML = statusHtml;
})
.catch(error => {
console.error('Error fetching system status:', error);
});
}
function loadChart(hours) {
// Update button states
document.querySelectorAll('.chart-controls button').forEach(btn => {
btn.classList.remove('active');
});
event.target.classList.add('active');
fetch(`/api/history/${hours}`)
.then(response => response.json())
.then(data => {
createTemperatureChart(data);
createHumidityChart(data);
createPressureChart(data);
})
.catch(error => {
console.error('Error fetching chart data:', error);
});
}
function createTemperatureChart(data) {
const ctx = document.getElementById('temperatureChart').getContext('2d');
if (temperatureChart) {
temperatureChart.destroy();
}
temperatureChart = new Chart(ctx, {
type: 'line',
data: {
labels: data.timestamps,
datasets: [{
label: 'Indoor Temperature (°C)',
data: data.indoor_temp,
borderColor: 'rgb(255, 99, 132)',
backgroundColor: 'rgba(255, 99, 132, 0.1)',
tension: 0.1
}, {
label: 'Outdoor Temperature (°C)',
data: data.outdoor_temp,
borderColor: 'rgb(54, 162, 235)',
backgroundColor: 'rgba(54, 162, 235, 0.1)',
tension: 0.1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: false,
title: {
display: true,
text: 'Temperature (°C)'
}
},
x: {
title: {
display: true,
text: 'Time'
}
}
}
}
});
}
function createHumidityChart(data) {
const ctx = document.getElementById('humidityChart').getContext('2d');
if (humidityChart) {
humidityChart.destroy();
}
humidityChart = new Chart(ctx, {
type: 'line',
data: {
labels: data.timestamps,
datasets: [{
label: 'Humidity (%)',
data: data.humidity,
borderColor: 'rgb(75, 192, 192)',
backgroundColor: 'rgba(75, 192, 192, 0.1)',
tension: 0.1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
max: 100,
title: {
display: true,
text: 'Humidity (%)'
}
},
x: {
title: {
display: true,
text: 'Time'
}
}
}
}
});
}
function createPressureChart(data) {
const ctx = document.getElementById('pressureChart').getContext('2d');
if (pressureChart) {
pressureChart.destroy();
}
pressureChart = new Chart(ctx, {
type: 'line',
data: {
labels: data.timestamps,
datasets: [{
label: 'Pressure (hPa)',
data: data.pressure,
borderColor: 'rgb(153, 102, 255)',
backgroundColor: 'rgba(153, 102, 255, 0.1)',
tension: 0.1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: false,
title: {
display: true,
text: 'Pressure (hPa)'
}
},
x: {
title: {
display: true,
text: 'Time'
}
}
}
}
});
}
Step 11: Set Up Auto-Start Services
Create systemd service for data collection:
sudo nano /etc/systemd/system/weather-station.service
[Unit]
Description=Weather Station Data Collection
After=network.target
[Service]
Type=simple
User=pi
WorkingDirectory=/home/pi/weather_station
Environment=PATH=/home/pi/weather_station/venv/bin
ExecStart=/home/pi/weather_station/venv/bin/python weather_station.py
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
Create systemd service for web dashboard:
sudo nano /etc/systemd/system/weather-dashboard.service
[Unit]
Description=Weather Station Web Dashboard
After=network.target
[Service]
Type=simple
User=pi
WorkingDirectory=/home/pi/weather_station
Environment=PATH=/home/pi/weather_station/venv/bin
ExecStart=/home/pi/weather_station/venv/bin/python web_dashboard.py
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
Enable and start services:
sudo systemctl enable weather-station.service
sudo systemctl enable weather-dashboard.service
sudo systemctl start weather-station.service
sudo systemctl start weather-dashboard.service
Check service status:
sudo systemctl status weather-station.service
sudo systemctl status weather-dashboard.service
Physical Installation and Weatherproofing
Enclosure Design
Indoor electronics box:
- Contains Raspberry Pi, power supply, and connections
- Good ventilation to prevent overheating
- Easy access for maintenance
- Cable glands for sensor wires
Outdoor sensor housing:
- IP65 rated weatherproof enclosure
- Solar radiation shield (Stevenson screen design)
- Ventilated but protected from direct sun and rain
- White color to reflect heat
- Mounted 4-6 feet above ground
Sensor Placement Best Practices
Temperature/Humidity sensors:
- Shade from direct sunlight
- Good air circulation
- Away from heat sources (buildings, pavement)
- 4-6 feet above ground
Light sensor:
- Unobstructed sky view
- Away from artificial lights
- Protected from direct rain
Outdoor temperature probe:
- Waterproof housing
- Away from building thermal effects
- Good air circulation
Power Options
Standard AC power:
- Weatherproof outlet near installation
- UPS backup for power outages
- Lowest cost option
Solar power system:
- 20W solar panel minimum
- 12V deep cycle battery (20Ah+)
- Charge controller
- DC-DC converter for Raspberry Pi
- Great for remote locations
Cable Management
Indoor to outdoor runs:
- Use outdoor-rated cables
- Seal entry points with silicone
- Protect from rodents and weather
- Leave service loops for maintenance
Advanced Features
Weather Alerts and Notifications
Email alerts:
import smtplib
from email.mime.text import MIMEText
def send_email_alert(subject, message):
smtp_server = "smtp.gmail.com"
smtp_port = 587
email = "your_email@gmail.com"
password = "your_app_password"
msg = MIMEText(message)
msg['Subject'] = subject
msg['From'] = email
msg['To'] = email
with smtplib.SMTP(smtp_server, smtp_port) as server:
server.starttls()
server.login(email, password)
server.send_message(msg)
SMS alerts via Twilio:
from twilio.rest import Client
def send_sms_alert(message):
account_sid = 'your_account_sid'
auth_token = 'your_auth_token'
client = Client(account_sid, auth_token)
message = client.messages.create(
body=message,
from_='+1234567890', # Your Twilio number
to='+0987654321' # Your phone number
)
Data Export and API
CSV export:
@app.route('/api/export/csv/<int:hours>')
def export_csv(hours):
data = db.get_recent_readings(hours)
# Convert to CSV format
# Return as downloadable file
Weather Underground upload:
def upload_to_weather_underground(data):
base_url = "http://weatherstation.wunderground.com/weatherstation/updateweatherstation.php"
params = {
'ID': 'YOUR_STATION_ID',
'PASSWORD': 'YOUR_PASSWORD',
'dateutc': 'now',
'tempf': celsius_to_fahrenheit(data['outdoor_temp']),
'humidity': data['humidity'],
'baromin': hpa_to_inches(data['pressure']),
'action': 'updateraw',
'softwaretype': 'RaspberryPi'
}
response = requests.get(base_url, params=params)
Additional Sensors
Wind measurement:
# Wind speed using Hall effect sensor
import RPi.GPIO as GPIO
class WindSpeedSensor:
def __init__(self, pin=21):
self.pin = pin
self.count = 0
GPIO.setmode(GPIO.BCM)
GPIO.setup(pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.add_event_detect(pin, GPIO.FALLING, callback=self.spin)
def spin(self, channel):
self.count += 1
def calculate_speed(self, time_period=5):
# Reset count and wait
self.count = 0
time.sleep(time_period)
# Calculate speed (calibration needed)
# 1 pulse = 1.492 mph for many anemometers
mph = (self.count * 1.492) / time_period
return mph * 1.609344 # Convert to km/h
Rain gauge:
class RainGauge:
def __init__(self, pin=20):
self.pin = pin
self.tips = 0
GPIO.setup(pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.add_event_detect(pin, GPIO.FALLING, callback=self.tip)
def tip(self, channel):
self.tips += 1
def get_rainfall_mm(self):
# Standard rain gauge: 0.2794mm per tip
return self.tips * 0.2794
def reset_daily(self):
self.tips = 0
Troubleshooting Guide
Sensor Issues
BME280 not detected:
- Check I2C wiring (SDA, SCL, power, ground)
- Verify I2C enabled:
sudo raspi-config - Test I2C:
sudo i2cdetect -y 1 - BME280 should appear at address 0x76 or 0x77
Inaccurate readings:
- Calibrate sensors against known references
- Check for sensor drift over time
- Ensure proper ventilation
- Avoid heat sources
Intermittent sensor failures:
- Check loose connections
- Inspect for corrosion (outdoor sensors)
- Add delay between sensor readings
- Implement retry logic
Network Issues
Dashboard not accessible:
- Check Flask service status
- Verify firewall settings
- Test port 5000 accessibility
- Check Pi's IP address
Database errors:
- Check disk space:
df -h - Verify database permissions
- Backup database regularly
- Monitor for corruption
Power Issues
System crashes:
- Check power supply capacity
- Monitor for voltage drops
- Add UPS for stability
- Check for overheating
Solar system not charging:
- Check panel orientation and cleanliness
- Verify charge controller settings
- Test battery condition
- Monitor charging current
Calibration and Accuracy
Sensor Calibration
Temperature calibration:
- Compare against certified thermometer
- Record offset at multiple temperatures
- Apply correction factor in software
- Re-calibrate annually
Humidity calibration:
- Use salt test (75% RH reference)
- Compare against hygrometer
- Apply offset correction
- Check for drift over time
Pressure calibration:
- Compare against local weather station
- Apply sea-level correction
- Account for elevation
- Cross-reference with meteorological data
Data Quality Control
Automated quality checks:
def quality_check(data):
"""Perform quality checks on sensor data"""
issues = []
# Temperature range checks
if data['indoor_temp'] and (data['indoor_temp'] < -40 or data['indoor_temp'] > 60):
issues.append("Indoor temperature out of range")
# Humidity range checks
if data['humidity'] and (data['humidity'] < 0 or data['humidity'] > 100):
issues.append("Humidity out of range")
# Pressure range checks
if data['pressure'] and (data['pressure'] < 800 or data['pressure'] > 1200):
issues.append("Pressure out of range")
# Rate of change checks
# (Compare with previous reading)
return issues
Cost Breakdown and ROI
Component Costs
Basic weather station ($85-120):
- Raspberry Pi 4: $45
- BME280 sensor: $12
- TSL2591 sensor: $10
- MicroSD card: $15
- Case and wiring: $15
- Basic enclosure: $20
- Total: $117
Enhanced weather station ($150-200):
- Add DS18B20 outdoor probe: $8
- Weatherproof enclosure: $25
- Professional mounting: $20
- Solar power option: $50
- Total: $170
vs. Commercial weather stations:
- Basic consumer station: $150-300
- Professional station: $500-2000+
- Advantage: Lower cost, complete customization
Return on Investment
Data value:
- Personal weather monitoring: Priceless
- Home automation integration: Saves energy
- Garden/greenhouse optimization: Better yields
- Educational value: Learning experience
Long-term savings:
- No subscription fees (many commercial stations charge monthly)
- Expandable without replacing entire system
- Repairable with common components
- Upgradeable sensors over time
Integration with Other Projects
Home Assistant Integration
# configuration.yaml
sensor:
- platform: rest
resource: http://192.168.1.50:5000/api/current
name: "Weather Station"
json_attributes_path: "$"
json_attributes:
- indoor_temp
- outdoor_temp
- humidity
- pressure
- light_lux
value_template: '{{ value_json.indoor_temp }}'
unit_of_measurement: '°C'
IoT Platform Integration
ThingSpeak upload:
def upload_to_thingspeak(data):
api_key = "YOUR_THINGSPEAK_API_KEY"
url = f"https://api.thingspeak.com/update?api_key={api_key}"
params = {
'field1': data['indoor_temp'],
'field2': data['outdoor_temp'],
'field3': data['humidity'],
'field4': data['pressure'],
'field5': data['light_lux']
}
response = requests.get(url, params=params)
Smart Home Automation
Trigger actions based on weather:
- Close blinds when light level high
- Turn on dehumidifier when humidity >70%
- Send frost alerts when temperature <0°C
- Adjust HVAC based on outdoor temperature
Frequently Asked Questions
What sensors should I start with?
BME280 is the best single sensor - it measures temperature, humidity, and pressure in one chip. Add TSL2591 for light measurement and DS18B20 for outdoor temperature.
How accurate are these sensors?
BME280 provides ±1°C temperature, ±3% humidity, and ±1 hPa pressure accuracy. This is sufficient for home weather monitoring and better than most consumer weather stations.
Can I use this data for official weather reporting?
These sensors are not certified for official meteorological use. However, they're accurate enough for personal use, home automation, and sharing with weather networks like Weather Underground.
How much power does the system use?
Raspberry Pi 4 uses 3-8W depending on load. With sensors, expect 8-12W total. Solar system needs 20W panel minimum with 20Ah+ battery for 24/7 operation.
Do I need programming experience?
Basic Python knowledge helps but isn't required. The provided code works as-is. Copy-paste the code, modify settings for your needs, and it will work.
How often should I take readings?
Every 1-5 minutes is typical. More frequent readings provide better data resolution but fill storage faster. Every minute provides good resolution for most applications.
Can I add more sensors later?
Absolutely! The modular design makes it easy to add wind speed, rain gauge, air quality, UV, or soil moisture sensors. Just modify the sensors.py file and database schema.
What's the range of the outdoor sensors?
Limited by cable length - typically 50-100 feet with standard wiring. Use shielded cable for longer runs, or wireless sensors for remote locations.
What's Next?
Expand Your Weather Station
Phase 1 additions:
- Wind speed and direction sensors
- Rain gauge for precipitation measurement
- UV sensor for sun exposure monitoring
- Soil temperature probes for gardening
Phase 2 enhancements:
- Webcam for sky conditions
- Lightning detector
- Air quality sensors (PM2.5, PM10)
- Multiple location monitoring
Phase 3 advanced features:
- Weather prediction algorithms
- Machine learning for local forecasting
- Integration with irrigation systems
- Weather balloon launches (advanced!)
Related Projects
Once you master weather monitoring, consider these related projects:
- Greenhouse automation - Climate control based on weather data
- Smart irrigation - Water plants based on weather and soil conditions
- Home energy management - Optimize heating/cooling with weather prediction
- Timelapse photography - Document weather changes over time
Join the Community
Share your data:
- Weather Underground citizen weather network
- APRS (Amateur Radio weather reporting)
- Local weather enthusiast groups
- Educational institutions
Learn more:
- Meteorology courses and books
- Weather prediction algorithms
- Climate monitoring techniques
- Sensor calibration and maintenance
Conclusion
Building your own weather station is more than just a fun project—it's a gateway to understanding the world around you. You'll gain insights into local weather patterns, learn valuable skills in electronics and programming, and have a useful tool for home automation and decision-making.
Key benefits of DIY weather monitoring:
- Educational: Learn about weather, sensors, programming, and data analysis
- Practical: Make informed decisions about outdoor activities, gardening, and home comfort
- Expandable: Start simple and add features as you learn
- Cost-effective: Better capabilities than commercial stations at lower cost
- Community: Share data and knowledge with weather enthusiasts worldwide
Remember: Weather monitoring is a long-term hobby. Your first station will teach you the basics, but you'll constantly find ways to improve accuracy, add features, and gain deeper insights into local climate patterns.
The most rewarding part isn't just collecting data—it's understanding what the data tells you about your local environment and using that knowledge to make better decisions every day.
Ready to build your weather station? Start with the basic BME280 setup and expand from there. Check out our related guides on home automation, data visualization, and IoT projects!
