TC
Troy’s Tech Corner
build tech2026-02-2715-20 min read

Build a DIY Weather Station with Raspberry Pi: Complete Tutorial

Understand Tech

Understand Tech

About the author

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

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:

  1. Sensors → Raspberry Pi GPIO/I2C
  2. Python script → Reads sensors every minute
  3. SQLite database → Stores timestamped measurements
  4. Flask web server → Provides dashboard interface
  5. 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:

  1. Use Raspberry Pi Imager
  2. Choose: Raspberry Pi OS (64-bit)
  3. Enable SSH and set credentials
  4. Configure Wi-Fi if not using ethernet
  5. 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:

  1. Compare against certified thermometer
  2. Record offset at multiple temperatures
  3. Apply correction factor in software
  4. Re-calibrate annually

Humidity calibration:

  1. Use salt test (75% RH reference)
  2. Compare against hygrometer
  3. Apply offset correction
  4. Check for drift over time

Pressure calibration:

  1. Compare against local weather station
  2. Apply sea-level correction
  3. Account for elevation
  4. 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!)

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!

Enjoyed this guide?

Get more beginner-friendly tech explanations and guides sent to your inbox.

No spam. Unsubscribe at any time. We respect your privacy.

Related Guides